comate-cli 0.7.5__tar.gz → 0.7.6__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.5 → comate_cli-0.7.6}/PKG-INFO +1 -1
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/app.py +5 -1
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/startup_profile.py +22 -8
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/update_check.py +114 -48
- {comate_cli-0.7.5 → comate_cli-0.7.6}/pyproject.toml +1 -1
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_app_startup_latency.py +41 -31
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_startup_profile.py +100 -1
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_update_check.py +167 -10
- {comate_cli-0.7.5 → comate_cli-0.7.6}/uv.lock +3 -415
- {comate_cli-0.7.5 → comate_cli-0.7.6}/.gitignore +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/CHANGELOG.md +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/README.md +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/__main__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/main.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/config/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/config/model.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/config/picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/config/picker_state.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/config/store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/event_renderer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/resume_picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/resume_preview.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/statusline/model.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/statusline/picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/statusline/picker_state.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/statusline/store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tool_result_store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/config/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/config/test_model.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/config/test_picker_state.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/config/test_picker_ui.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/config/test_roundtrip.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/config/test_store_load.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/config/test_store_save.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/conftest.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/statusline/__init__.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/statusline/test_model.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/statusline/test_picker_state.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/statusline/test_store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_app_token_cost_config.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_btw_slash_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_context_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_event_renderer_log_boundary.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_event_renderer_log_queue.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_event_renderer_streaming.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_event_renderer_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_format_error.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_goal_resume_tui.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_goal_resume_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_goal_slash_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_handle_error.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_history_printer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_history_printer_log.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_history_printer_subtitle_position.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_history_printer_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_history_sync.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_history_sync_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_input_history.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_logo.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_main_args.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_preflight.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_question_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_resume_picker.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_resume_preview.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_session_query_token_summary.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_shutdown_noise_guard.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_shutdown_noise_integration.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_slash_clear.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_status_bar.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_task_poll.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tool_fold_panel.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tool_result_formatters.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tool_result_store.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tool_result_viewer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tool_result_viewer_key_bindings.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tool_view.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_transcript_viewer.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_transcript_viewer_tool_fold.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_esc_queue.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_paste_newline_guard.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_queue_sdk_source.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_startup_latency.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_team_messages.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_thinking_display.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
- {comate_cli-0.7.5 → comate_cli-0.7.6}/tests/test_usage_command.py +0 -0
|
@@ -42,6 +42,7 @@ from comate_cli.terminal_agent.update_check import (
|
|
|
42
42
|
|
|
43
43
|
console = Console()
|
|
44
44
|
logger = logging.getLogger(__name__)
|
|
45
|
+
_UPDATE_CHECK_DELAY_S = 1.0
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def _resolve_cli_project_root() -> Path:
|
|
@@ -69,7 +70,7 @@ async def _handle_update_on_launch(info: UpdateInfo) -> bool:
|
|
|
69
70
|
if decision == UpdatePromptDecision.SKIP:
|
|
70
71
|
return False
|
|
71
72
|
if decision == UpdatePromptDecision.SHOW_HINT:
|
|
72
|
-
console.print(format_update_hint(info))
|
|
73
|
+
console.print(f"[dim]{format_update_hint(info)}[/]")
|
|
73
74
|
mark_update_seen(info)
|
|
74
75
|
return False
|
|
75
76
|
|
|
@@ -115,6 +116,9 @@ def _schedule_update_check_on_launch(
|
|
|
115
116
|
async def _run() -> None:
|
|
116
117
|
try:
|
|
117
118
|
profiler.mark("update_check.start")
|
|
119
|
+
profiler.mark("update_check.delay.start")
|
|
120
|
+
await asyncio.sleep(_UPDATE_CHECK_DELAY_S)
|
|
121
|
+
profiler.mark("update_check.delay.done")
|
|
118
122
|
update_info = await _check_update(profiler=profiler.child("update_check"))
|
|
119
123
|
profiler.mark("update_check.done")
|
|
120
124
|
if update_info is not None:
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import threading
|
|
5
6
|
import time
|
|
6
7
|
|
|
7
8
|
|
|
@@ -17,12 +18,14 @@ class StartupProfiler:
|
|
|
17
18
|
started_at: float | None = None,
|
|
18
19
|
prefix: str = "",
|
|
19
20
|
pending_messages: list[str] | None = None,
|
|
21
|
+
lock: threading.Lock | None = None,
|
|
20
22
|
) -> None:
|
|
21
23
|
self._logger = logger
|
|
22
24
|
self._enabled = enabled
|
|
23
25
|
self._started_at = started_at if started_at is not None else time.perf_counter()
|
|
24
26
|
self._prefix = prefix.strip(".")
|
|
25
27
|
self._pending_messages = pending_messages if pending_messages is not None else []
|
|
28
|
+
self._lock = lock if lock is not None else threading.Lock()
|
|
26
29
|
|
|
27
30
|
@classmethod
|
|
28
31
|
def from_env(cls, *, logger: logging.Logger) -> "StartupProfiler":
|
|
@@ -37,20 +40,30 @@ class StartupProfiler:
|
|
|
37
40
|
if self._prefix:
|
|
38
41
|
normalized_phase = f"{self._prefix}.{normalized_phase}"
|
|
39
42
|
elapsed_ms = (time.perf_counter() - self._started_at) * 1000
|
|
40
|
-
self.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
with self._lock:
|
|
44
|
+
self._pending_messages.append(
|
|
45
|
+
f"startup_profile phase={normalized_phase} elapsed_ms={elapsed_ms:.3f}"
|
|
46
|
+
)
|
|
47
|
+
pending = self._drain_pending_locked()
|
|
48
|
+
for message in pending:
|
|
49
|
+
self._logger.info(message)
|
|
44
50
|
|
|
45
51
|
def flush(self) -> None:
|
|
46
|
-
if not self._enabled
|
|
52
|
+
if not self._enabled:
|
|
47
53
|
return
|
|
54
|
+
with self._lock:
|
|
55
|
+
pending = self._drain_pending_locked()
|
|
56
|
+
for message in pending:
|
|
57
|
+
self._logger.info(message)
|
|
58
|
+
|
|
59
|
+
def _drain_pending_locked(self) -> list[str]:
|
|
60
|
+
if not self._pending_messages:
|
|
61
|
+
return []
|
|
48
62
|
if not self._logger.isEnabledFor(logging.INFO):
|
|
49
|
-
return
|
|
63
|
+
return []
|
|
50
64
|
pending = list(self._pending_messages)
|
|
51
65
|
self._pending_messages.clear()
|
|
52
|
-
|
|
53
|
-
self._logger.info(message)
|
|
66
|
+
return pending
|
|
54
67
|
|
|
55
68
|
def child(self, prefix: str) -> "StartupProfiler":
|
|
56
69
|
normalized_prefix = prefix.strip(".")
|
|
@@ -64,4 +77,5 @@ class StartupProfiler:
|
|
|
64
77
|
started_at=self._started_at,
|
|
65
78
|
prefix=normalized_prefix,
|
|
66
79
|
pending_messages=self._pending_messages,
|
|
80
|
+
lock=self._lock,
|
|
67
81
|
)
|
|
@@ -24,6 +24,8 @@ PACKAGE_NAME = "comate-cli"
|
|
|
24
24
|
RELEASE_NOTES_URL = "https://github.com/AndyLee1024/agent-sdk/releases/latest"
|
|
25
25
|
UPDATE_COMMAND = ("uv", "tool", "update", PACKAGE_NAME)
|
|
26
26
|
_SETTINGS_SECTION = "updates"
|
|
27
|
+
_UPDATE_CHECK_TRUST_ENV = "COMATE_CLI_UPDATE_CHECK_TRUST_ENV"
|
|
28
|
+
_TRUTHY_ENV_VALUES = {"1", "true", "yes", "on"}
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class UpdatePromptDecision(Enum):
|
|
@@ -65,6 +67,11 @@ def _is_chinese_locale() -> bool:
|
|
|
65
67
|
return False
|
|
66
68
|
|
|
67
69
|
|
|
70
|
+
def _update_check_trust_env() -> bool:
|
|
71
|
+
raw = os.environ.get(_UPDATE_CHECK_TRUST_ENV, "")
|
|
72
|
+
return raw.strip().lower() in _TRUTHY_ENV_VALUES
|
|
73
|
+
|
|
74
|
+
|
|
68
75
|
async def check_update(
|
|
69
76
|
*,
|
|
70
77
|
package: str = PACKAGE_NAME,
|
|
@@ -73,77 +80,136 @@ async def check_update(
|
|
|
73
80
|
profiler: Any | None = None,
|
|
74
81
|
) -> UpdateInfo | None:
|
|
75
82
|
"""异步检查 comate-cli 是否有新版本,返回结构化版本信息或 None。"""
|
|
76
|
-
|
|
83
|
+
if profiler is not None:
|
|
84
|
+
profiler.mark("thread.await.start")
|
|
77
85
|
try:
|
|
86
|
+
return await asyncio.to_thread(
|
|
87
|
+
check_update_blocking,
|
|
88
|
+
package=package,
|
|
89
|
+
release_notes_url=release_notes_url,
|
|
90
|
+
log=log,
|
|
91
|
+
profiler=profiler,
|
|
92
|
+
)
|
|
93
|
+
except asyncio.CancelledError:
|
|
78
94
|
if profiler is not None:
|
|
79
|
-
profiler.mark("
|
|
80
|
-
|
|
81
|
-
except
|
|
95
|
+
profiler.mark("thread.await.cancelled")
|
|
96
|
+
raise
|
|
97
|
+
except Exception:
|
|
98
|
+
if profiler is not None:
|
|
99
|
+
profiler.mark("thread.await.failed")
|
|
100
|
+
active_logger = log or logger
|
|
101
|
+
active_logger.debug("update check worker failed", exc_info=True)
|
|
82
102
|
return None
|
|
83
103
|
finally:
|
|
84
104
|
if profiler is not None:
|
|
85
|
-
profiler.mark("
|
|
105
|
+
profiler.mark("thread.await.done")
|
|
86
106
|
|
|
87
|
-
if profiler is not None:
|
|
88
|
-
profiler.mark("locale.start")
|
|
89
|
-
if _is_chinese_locale():
|
|
90
|
-
url = f"https://mirrors.tuna.tsinghua.edu.cn/pypi/{package}/json"
|
|
91
|
-
source_label = "tuna"
|
|
92
|
-
else:
|
|
93
|
-
url = f"https://pypi.org/pypi/{package}/json"
|
|
94
|
-
source_label = "pypi"
|
|
95
|
-
if profiler is not None:
|
|
96
|
-
profiler.mark("locale.done")
|
|
97
107
|
|
|
108
|
+
def check_update_blocking(
|
|
109
|
+
*,
|
|
110
|
+
package: str = PACKAGE_NAME,
|
|
111
|
+
release_notes_url: str = RELEASE_NOTES_URL,
|
|
112
|
+
log: logging.Logger | None = None,
|
|
113
|
+
profiler: Any | None = None,
|
|
114
|
+
) -> UpdateInfo | None:
|
|
115
|
+
"""同步执行版本检查;由 async wrapper 放入 worker thread,避免阻塞 TUI event loop。"""
|
|
116
|
+
active_logger = log or logger
|
|
98
117
|
try:
|
|
99
118
|
if profiler is not None:
|
|
100
|
-
profiler.mark("
|
|
101
|
-
import httpx
|
|
102
|
-
if profiler is not None:
|
|
103
|
-
profiler.mark("httpx_import.done")
|
|
119
|
+
profiler.mark("thread.run.start")
|
|
104
120
|
|
|
105
|
-
|
|
121
|
+
try:
|
|
106
122
|
if profiler is not None:
|
|
107
|
-
profiler.mark("
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
profiler.mark("current_version.start")
|
|
124
|
+
current = importlib.metadata.version(package)
|
|
125
|
+
except importlib.metadata.PackageNotFoundError:
|
|
126
|
+
return None
|
|
127
|
+
finally:
|
|
111
128
|
if profiler is not None:
|
|
112
|
-
profiler.mark("
|
|
113
|
-
except Exception:
|
|
114
|
-
active_logger.debug(f"update check failed (source={source_label})", exc_info=True)
|
|
115
|
-
return None
|
|
129
|
+
profiler.mark("current_version.done")
|
|
116
130
|
|
|
117
|
-
try:
|
|
118
131
|
if profiler is not None:
|
|
119
|
-
profiler.mark("
|
|
120
|
-
|
|
132
|
+
profiler.mark("locale.start")
|
|
133
|
+
if _is_chinese_locale():
|
|
134
|
+
url = f"https://mirrors.tuna.tsinghua.edu.cn/pypi/{package}/json"
|
|
135
|
+
source_label = "tuna"
|
|
136
|
+
else:
|
|
137
|
+
url = f"https://pypi.org/pypi/{package}/json"
|
|
138
|
+
source_label = "pypi"
|
|
139
|
+
if profiler is not None:
|
|
140
|
+
profiler.mark("locale.done")
|
|
121
141
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
try:
|
|
143
|
+
if profiler is not None:
|
|
144
|
+
profiler.mark("httpx_import.start")
|
|
145
|
+
import httpx
|
|
146
|
+
if profiler is not None:
|
|
147
|
+
profiler.mark("httpx_import.done")
|
|
148
|
+
|
|
149
|
+
trust_env = _update_check_trust_env()
|
|
150
|
+
if profiler is not None:
|
|
151
|
+
profiler.mark(
|
|
152
|
+
"http_client.trust_env.enabled"
|
|
153
|
+
if trust_env
|
|
154
|
+
else "http_client.trust_env.disabled"
|
|
155
|
+
)
|
|
156
|
+
profiler.mark("http_client.construct.start")
|
|
157
|
+
client = httpx.Client(timeout=3.0, trust_env=trust_env)
|
|
158
|
+
if profiler is not None:
|
|
159
|
+
profiler.mark("http_client.construct.done")
|
|
160
|
+
|
|
161
|
+
if profiler is not None:
|
|
162
|
+
profiler.mark("http_client.enter.start")
|
|
163
|
+
with client:
|
|
164
|
+
if profiler is not None:
|
|
165
|
+
profiler.mark("http_client.enter.done")
|
|
166
|
+
profiler.mark("http_get.start")
|
|
167
|
+
resp = client.get(url)
|
|
168
|
+
resp.raise_for_status()
|
|
169
|
+
latest = resp.json()["info"]["version"]
|
|
170
|
+
if profiler is not None:
|
|
171
|
+
profiler.mark("http_get.done")
|
|
172
|
+
except Exception:
|
|
173
|
+
if profiler is not None:
|
|
174
|
+
profiler.mark("http_client.failed")
|
|
175
|
+
active_logger.debug(f"update check failed (source={source_label})", exc_info=True)
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
if profiler is not None:
|
|
180
|
+
profiler.mark("version_compare.start")
|
|
181
|
+
from packaging.version import Version
|
|
182
|
+
|
|
183
|
+
if Version(latest) > Version(current):
|
|
184
|
+
return UpdateInfo(
|
|
185
|
+
package=package,
|
|
186
|
+
current_version=current,
|
|
187
|
+
latest_version=latest,
|
|
188
|
+
release_notes_url=release_notes_url,
|
|
189
|
+
)
|
|
190
|
+
except Exception:
|
|
191
|
+
active_logger.debug(
|
|
192
|
+
"update comparison failed (current=%s, latest=%s)",
|
|
193
|
+
current,
|
|
194
|
+
latest,
|
|
195
|
+
exc_info=True,
|
|
128
196
|
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
latest,
|
|
134
|
-
exc_info=True,
|
|
135
|
-
)
|
|
197
|
+
finally:
|
|
198
|
+
if profiler is not None:
|
|
199
|
+
profiler.mark("version_compare.done")
|
|
200
|
+
return None
|
|
136
201
|
finally:
|
|
137
202
|
if profiler is not None:
|
|
138
|
-
profiler.mark("
|
|
139
|
-
return None
|
|
203
|
+
profiler.mark("thread.run.done")
|
|
140
204
|
|
|
141
205
|
|
|
142
206
|
def format_update_hint(info: UpdateInfo) -> str:
|
|
207
|
+
# Returns plain text only. Callers are responsible for adding markup
|
|
208
|
+
# appropriate to their render target (Rich Console vs prompt_toolkit).
|
|
143
209
|
return (
|
|
144
|
-
f"
|
|
210
|
+
f"New version available: {info.latest_version} "
|
|
145
211
|
f"(current: {info.current_version}) "
|
|
146
|
-
f"Run
|
|
212
|
+
f"Run `uv tool update {info.package}` to update."
|
|
147
213
|
)
|
|
148
214
|
|
|
149
215
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import threading
|
|
4
5
|
import unittest
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from types import SimpleNamespace
|
|
@@ -140,8 +141,8 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
|
|
|
140
141
|
async def test_pending_update_check_does_not_block_tui_run(self) -> None:
|
|
141
142
|
session = _fake_session()
|
|
142
143
|
created_tuis: list[_FakeTUI] = []
|
|
143
|
-
update_started =
|
|
144
|
-
|
|
144
|
+
update_started = threading.Event()
|
|
145
|
+
release_update = threading.Event()
|
|
145
146
|
|
|
146
147
|
class _RecordingTUI(_FakeTUI):
|
|
147
148
|
def __init__(self, session, status_bar, renderer) -> None:
|
|
@@ -150,41 +151,48 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
|
|
|
150
151
|
|
|
151
152
|
async def run(self, *, mcp_init, profiler=None) -> None:
|
|
152
153
|
del mcp_init, profiler
|
|
153
|
-
await asyncio.wait_for(update_started.wait(), timeout=0.1)
|
|
154
154
|
self.run_called.set()
|
|
155
|
+
assert not update_started.is_set()
|
|
156
|
+
assert await asyncio.to_thread(update_started.wait, 0.2)
|
|
157
|
+
release_update.set()
|
|
158
|
+
await asyncio.sleep(0)
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
del
|
|
160
|
+
def _slow_blocking_check(**kwargs) -> UpdateInfo | None:
|
|
161
|
+
del kwargs
|
|
158
162
|
update_started.set()
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
163
|
+
release_update.wait(timeout=1.0)
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
with (
|
|
168
|
+
patch.object(app_module, "_UPDATE_CHECK_DELAY_S", 0.01),
|
|
169
|
+
patch.object(app_module, "_resolve_cli_project_root", return_value=Path("/tmp/project")),
|
|
170
|
+
patch.object(
|
|
171
|
+
app_module,
|
|
172
|
+
"run_preflight_if_needed",
|
|
173
|
+
AsyncMock(return_value=PreflightResult(status="configured")),
|
|
174
|
+
),
|
|
175
|
+
patch.object(app_module, "_build_agent", return_value=object()),
|
|
176
|
+
patch.object(app_module, "print_logo"),
|
|
177
|
+
patch(
|
|
178
|
+
"comate_cli.terminal_agent.update_check.check_update_blocking",
|
|
179
|
+
side_effect=_slow_blocking_check,
|
|
180
|
+
),
|
|
181
|
+
patch.object(app_module, "EventRenderer", return_value=MagicMock()),
|
|
182
|
+
patch("comate_cli.terminal_agent.logging_adapter.setup_tui_logging", return_value=MagicMock()),
|
|
183
|
+
patch.object(app_module, "_resolve_session", return_value=(session, "new")),
|
|
184
|
+
patch.object(app_module, "StatusBar", _FakeStatusBar),
|
|
185
|
+
patch.object(app_module, "TerminalAgentTUI", _RecordingTUI),
|
|
186
|
+
patch.object(app_module, "_graceful_shutdown", AsyncMock()),
|
|
187
|
+
patch.object(app_module, "_format_exit_usage_line", return_value=None),
|
|
188
|
+
):
|
|
189
|
+
await asyncio.wait_for(app_module.run(), timeout=0.4)
|
|
190
|
+
finally:
|
|
191
|
+
release_update.set()
|
|
184
192
|
|
|
185
193
|
self.assertEqual(len(created_tuis), 1)
|
|
186
194
|
self.assertTrue(created_tuis[0].run_called.is_set())
|
|
187
|
-
self.assertTrue(
|
|
195
|
+
self.assertTrue(update_started.is_set())
|
|
188
196
|
|
|
189
197
|
async def test_update_check_exception_does_not_block_tui_run(self) -> None:
|
|
190
198
|
session = _fake_session()
|
|
@@ -232,6 +240,7 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
|
|
|
232
240
|
)
|
|
233
241
|
|
|
234
242
|
with (
|
|
243
|
+
patch.object(app_module, "_UPDATE_CHECK_DELAY_S", 0.01),
|
|
235
244
|
patch.object(app_module, "_check_update", AsyncMock(return_value=info)),
|
|
236
245
|
patch.object(app_module, "decide_update_prompt", return_value=UpdatePromptDecision.SHOW_HINT),
|
|
237
246
|
patch.object(app_module, "mark_update_seen") as mark_seen,
|
|
@@ -256,6 +265,7 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
|
|
|
256
265
|
)
|
|
257
266
|
|
|
258
267
|
with (
|
|
268
|
+
patch.object(app_module, "_UPDATE_CHECK_DELAY_S", 0.01),
|
|
259
269
|
patch.object(app_module, "_check_update", AsyncMock(return_value=info)),
|
|
260
270
|
patch.object(app_module, "decide_update_prompt", return_value=UpdatePromptDecision.SHOW_PROMPT),
|
|
261
271
|
patch.object(app_module, "show_update_prompt", AsyncMock()) as show_prompt,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
5
6
|
import unittest
|
|
6
7
|
from contextlib import contextmanager
|
|
7
8
|
from pathlib import Path
|
|
@@ -129,6 +130,104 @@ class TestStartupProfiler(unittest.TestCase):
|
|
|
129
130
|
self.assertIn("phase=agent.build.start", messages[0])
|
|
130
131
|
self.assertIn("phase=logging.setup.done", messages[1])
|
|
131
132
|
|
|
133
|
+
def test_flush_releases_buffered_marks_after_info_logging_is_enabled(self) -> None:
|
|
134
|
+
logger = logging.getLogger("comate_cli.tests.startup_profile.flush")
|
|
135
|
+
original_level = logger.level
|
|
136
|
+
original_propagate = logger.propagate
|
|
137
|
+
records: list[logging.LogRecord] = []
|
|
138
|
+
|
|
139
|
+
class _ListHandler(logging.Handler):
|
|
140
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
141
|
+
records.append(record)
|
|
142
|
+
|
|
143
|
+
handler = _ListHandler(level=logging.INFO)
|
|
144
|
+
logger.handlers[:] = []
|
|
145
|
+
logger.addHandler(handler)
|
|
146
|
+
logger.propagate = False
|
|
147
|
+
logger.setLevel(logging.WARNING)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
with (
|
|
151
|
+
patch.dict("os.environ", {"COMATE_STARTUP_PROFILE": "1"}, clear=False),
|
|
152
|
+
patch("comate_cli.terminal_agent.startup_profile.time.perf_counter", side_effect=[3.0, 3.01]),
|
|
153
|
+
):
|
|
154
|
+
profiler = StartupProfiler.from_env(logger=logger)
|
|
155
|
+
profiler.mark("pre_logging.phase")
|
|
156
|
+
|
|
157
|
+
self.assertEqual(records, [])
|
|
158
|
+
|
|
159
|
+
logger.setLevel(logging.INFO)
|
|
160
|
+
profiler.flush()
|
|
161
|
+
|
|
162
|
+
messages = [record.getMessage() for record in records]
|
|
163
|
+
finally:
|
|
164
|
+
logger.removeHandler(handler)
|
|
165
|
+
logger.setLevel(original_level)
|
|
166
|
+
logger.propagate = original_propagate
|
|
167
|
+
|
|
168
|
+
self.assertEqual(len(messages), 1)
|
|
169
|
+
self.assertIn("phase=pre_logging.phase", messages[0])
|
|
170
|
+
|
|
171
|
+
def test_child_profiler_shares_thread_lock(self) -> None:
|
|
172
|
+
logger = MagicMock(spec=logging.Logger)
|
|
173
|
+
|
|
174
|
+
with patch.dict("os.environ", {"COMATE_STARTUP_PROFILE": "1"}, clear=False):
|
|
175
|
+
profiler = StartupProfiler.from_env(logger=logger)
|
|
176
|
+
|
|
177
|
+
child = profiler.child("worker")
|
|
178
|
+
|
|
179
|
+
self.assertIs(child._lock, profiler._lock)
|
|
180
|
+
|
|
181
|
+
def test_child_marks_from_threads_do_not_drop_phases(self) -> None:
|
|
182
|
+
logger = logging.getLogger("comate_cli.tests.startup_profile.threaded")
|
|
183
|
+
original_level = logger.level
|
|
184
|
+
original_propagate = logger.propagate
|
|
185
|
+
records: list[logging.LogRecord] = []
|
|
186
|
+
records_lock = threading.Lock()
|
|
187
|
+
|
|
188
|
+
class _ListHandler(logging.Handler):
|
|
189
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
190
|
+
with records_lock:
|
|
191
|
+
records.append(record)
|
|
192
|
+
|
|
193
|
+
handler = _ListHandler(level=logging.INFO)
|
|
194
|
+
logger.handlers[:] = []
|
|
195
|
+
logger.addHandler(handler)
|
|
196
|
+
logger.propagate = False
|
|
197
|
+
logger.setLevel(logging.INFO)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
with patch.dict("os.environ", {"COMATE_STARTUP_PROFILE": "1"}, clear=False):
|
|
201
|
+
profiler = StartupProfiler.from_env(logger=logger)
|
|
202
|
+
|
|
203
|
+
def _mark(prefix: str, count: int) -> None:
|
|
204
|
+
child = profiler.child(prefix)
|
|
205
|
+
for index in range(count):
|
|
206
|
+
child.mark(f"phase{index}")
|
|
207
|
+
|
|
208
|
+
threads = [
|
|
209
|
+
threading.Thread(target=_mark, args=("worker_a", 20)),
|
|
210
|
+
threading.Thread(target=_mark, args=("worker_b", 20)),
|
|
211
|
+
]
|
|
212
|
+
for thread in threads:
|
|
213
|
+
thread.start()
|
|
214
|
+
for thread in threads:
|
|
215
|
+
thread.join()
|
|
216
|
+
|
|
217
|
+
messages = [record.getMessage() for record in records]
|
|
218
|
+
finally:
|
|
219
|
+
logger.removeHandler(handler)
|
|
220
|
+
logger.setLevel(original_level)
|
|
221
|
+
logger.propagate = original_propagate
|
|
222
|
+
|
|
223
|
+
self.assertEqual(len(messages), 40)
|
|
224
|
+
for prefix in ("worker_a", "worker_b"):
|
|
225
|
+
for index in range(20):
|
|
226
|
+
self.assertTrue(
|
|
227
|
+
any(f"phase={prefix}.phase{index}" in message for message in messages),
|
|
228
|
+
f"missing {prefix}.phase{index}: {messages}",
|
|
229
|
+
)
|
|
230
|
+
|
|
132
231
|
|
|
133
232
|
class TestStartupProfilerTUI(unittest.IsolatedAsyncioTestCase):
|
|
134
233
|
async def test_tui_run_marks_run_async_boundary(self) -> None:
|