linhai 0.1.0__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.
- linhai-0.1.0/.gitea/scripts/check_any_object_type_annotations.py +194 -0
- linhai-0.1.0/.gitea/scripts/check_black_overformat.py +89 -0
- linhai-0.1.0/.gitea/scripts/check_branch_behind.sh +15 -0
- linhai-0.1.0/.gitea/scripts/check_cast_usage.py +167 -0
- linhai-0.1.0/.gitea/scripts/check_code_emoji.py +85 -0
- linhai-0.1.0/.gitea/scripts/check_commit_count.sh +34 -0
- linhai-0.1.0/.gitea/scripts/check_create_task_usage.py +156 -0
- linhai-0.1.0/.gitea/scripts/check_e2e_skip.py +121 -0
- linhai-0.1.0/.gitea/scripts/check_emoji.py +64 -0
- linhai-0.1.0/.gitea/scripts/check_empty_pr.sh +35 -0
- linhai-0.1.0/.gitea/scripts/check_git_diff.sh +66 -0
- linhai-0.1.0/.gitea/scripts/check_none_default_args.py +192 -0
- linhai-0.1.0/.gitea/scripts/check_pr_body.py +28 -0
- linhai-0.1.0/.gitea/scripts/check_pr_issue.py +91 -0
- linhai-0.1.0/.gitea/scripts/check_pr_tests.sh +29 -0
- linhai-0.1.0/.gitea/scripts/check_python_comments.py +143 -0
- linhai-0.1.0/.gitea/scripts/check_snapshot_docs.py +90 -0
- linhai-0.1.0/.gitea/scripts/test_check_scripts.py +330 -0
- linhai-0.1.0/.gitea/workflows/ci.yaml +274 -0
- linhai-0.1.0/.gitignore +12 -0
- linhai-0.1.0/.pylintrc +3 -0
- linhai-0.1.0/1031218.cast +1244 -0
- linhai-0.1.0/AGENTS.md +91 -0
- linhai-0.1.0/CODE_REQUIREMENTS.md +39 -0
- linhai-0.1.0/LICENSE +674 -0
- linhai-0.1.0/MESSAGE_DESIGN.md +143 -0
- linhai-0.1.0/PKG-INFO +33 -0
- linhai-0.1.0/PROJECT.md +48 -0
- linhai-0.1.0/README.md +111 -0
- linhai-0.1.0/README_zh-CN.md +104 -0
- linhai-0.1.0/app.diff +28 -0
- linhai-0.1.0/assets/demo.gif +0 -0
- linhai-0.1.0/assets/social-preview.jpg +0 -0
- linhai-0.1.0/assets/social-preview.xcf +0 -0
- linhai-0.1.0/demo.gif +0 -0
- linhai-0.1.0/demo_compressed.gif +0 -0
- linhai-0.1.0/docs/LETHAL_TRIFECTA.md +36 -0
- linhai-0.1.0/docs/QWEN35_CONTEXT_CACHE.md +263 -0
- linhai-0.1.0/docs/claude_constitution/article.md +258 -0
- linhai-0.1.0/docs/claude_constitution/constitution.md +3280 -0
- linhai-0.1.0/docs/claude_constitution/cybersecurity_summary.md +78 -0
- linhai-0.1.0/docs/deepseek_tokenizer.md +36 -0
- linhai-0.1.0/docs/openclaw-core-markdown/AGENTS.md +214 -0
- linhai-0.1.0/docs/openclaw-core-markdown/BOOTSTRAP.md +55 -0
- linhai-0.1.0/docs/openclaw-core-markdown/IDENTITY.md +18 -0
- linhai-0.1.0/docs/openclaw-core-markdown/SOUL.md +36 -0
- linhai-0.1.0/docs/openclaw-core-markdown/USER.md +17 -0
- linhai-0.1.0/docs/telegram/README.md +86 -0
- linhai-0.1.0/docs/telegram/TESTING.md +171 -0
- linhai-0.1.0/docs/telegram/test_botcommand.py +85 -0
- linhai-0.1.0/docs/telegram/test_callbackquery.py +197 -0
- linhai-0.1.0/docs/telegram/test_conversationhandler.py +173 -0
- linhai-0.1.0/docs/telegram/test_filters.py +221 -0
- linhai-0.1.0/docs/telegram/test_messagehandler.py +200 -0
- linhai-0.1.0/docs/tiktoken-docs.md +131 -0
- linhai-0.1.0/docs/tiktoken.md +37 -0
- linhai-0.1.0/e2e/conftest.py +139 -0
- linhai-0.1.0/e2e/machine_control/test_bash_host_concurrent_e2e.py +102 -0
- linhai-0.1.0/e2e/machine_control/test_bash_host_e2e.py +213 -0
- linhai-0.1.0/e2e/machine_control/test_bash_terminal_e2e.py +132 -0
- linhai-0.1.0/e2e/machine_control/test_posix_shell_e2e.py +108 -0
- linhai-0.1.0/e2e/machine_control/test_pty_connect_e2e.py +102 -0
- linhai-0.1.0/e2e/machine_control/test_trojan_concurrent_e2e.py +105 -0
- linhai-0.1.0/e2e/machine_control/test_trojan_pty.py +268 -0
- linhai-0.1.0/e2e/openai_toolcall/__init__.py +0 -0
- linhai-0.1.0/e2e/openai_toolcall/test_openai_toolcall_e2e.py +253 -0
- linhai-0.1.0/e2e/openai_toolcall/test_toolcall_tmux_e2e.py +133 -0
- linhai-0.1.0/e2e/test_agent_loop.py +153 -0
- linhai-0.1.0/e2e/test_conversation_restore.py +223 -0
- linhai-0.1.0/e2e/test_git_worktree.py +125 -0
- linhai-0.1.0/e2e/test_llm_connection.py +120 -0
- linhai-0.1.0/e2e/test_mcp_connection.py +149 -0
- linhai-0.1.0/e2e/test_mcp_server.py +39 -0
- linhai-0.1.0/e2e/test_openai_native_toolcall.py +138 -0
- linhai-0.1.0/e2e/test_process.py +291 -0
- linhai-0.1.0/e2e/test_process_toolset.py +221 -0
- linhai-0.1.0/e2e/test_smoke.py +15 -0
- linhai-0.1.0/e2e/test_terminal.py +60 -0
- linhai-0.1.0/e2e/test_tool_calling.py +138 -0
- linhai-0.1.0/e2e/test_user_plugins.py +138 -0
- linhai-0.1.0/e2e/test_webui.py +81 -0
- linhai-0.1.0/e2e/test_webui_api.py +184 -0
- linhai-0.1.0/e2e/test_webui_status_bar.py +125 -0
- linhai-0.1.0/e2e/test_webui_streaming.py +385 -0
- linhai-0.1.0/e2e/test_with_secret_split_e2e.py +234 -0
- linhai-0.1.0/flake.lock +276 -0
- linhai-0.1.0/flake.nix +132 -0
- linhai-0.1.0/linhai/__init__.py +3 -0
- linhai-0.1.0/linhai/__main__.py +10 -0
- linhai-0.1.0/linhai/agent/__init__.py +23 -0
- linhai-0.1.0/linhai/agent/answer.py +120 -0
- linhai-0.1.0/linhai/agent/callback_slot.py +74 -0
- linhai-0.1.0/linhai/agent/command_callback.py +177 -0
- linhai-0.1.0/linhai/agent/conversation.py +152 -0
- linhai-0.1.0/linhai/agent/conversation_save.py +58 -0
- linhai-0.1.0/linhai/agent/create.py +757 -0
- linhai-0.1.0/linhai/agent/lifecycle.py +185 -0
- linhai-0.1.0/linhai/agent/main.py +335 -0
- linhai-0.1.0/linhai/agent/message.py +467 -0
- linhai-0.1.0/linhai/agent/messages/__init__.py +16 -0
- linhai-0.1.0/linhai/agent/messages/compression.py +64 -0
- linhai-0.1.0/linhai/agent/messages/file_content.py +57 -0
- linhai-0.1.0/linhai/agent/messages/prompt.py +67 -0
- linhai-0.1.0/linhai/agent/messages/reasoning.py +63 -0
- linhai-0.1.0/linhai/agent/messages/runtime.py +31 -0
- linhai-0.1.0/linhai/agent/orchestration.py +797 -0
- linhai-0.1.0/linhai/agent/planning.py +88 -0
- linhai-0.1.0/linhai/agent/savable_state.py +8 -0
- linhai-0.1.0/linhai/agent/state_machine.py +108 -0
- linhai-0.1.0/linhai/agent/toolcall.py +578 -0
- linhai-0.1.0/linhai/agent/user_message_handler.py +44 -0
- linhai-0.1.0/linhai/agent/workflow.py +275 -0
- linhai-0.1.0/linhai/base.py +541 -0
- linhai-0.1.0/linhai/cl100k_base.tiktoken +100256 -0
- linhai-0.1.0/linhai/config.py +506 -0
- linhai-0.1.0/linhai/context_statistics.py +314 -0
- linhai-0.1.0/linhai/cron.py +168 -0
- linhai-0.1.0/linhai/exceptions.py +19 -0
- linhai-0.1.0/linhai/init/__init__.py +14 -0
- linhai-0.1.0/linhai/init/app.py +160 -0
- linhai-0.1.0/linhai/init/config_writer.py +178 -0
- linhai-0.1.0/linhai/init/widgets.py +118 -0
- linhai-0.1.0/linhai/llm.py +642 -0
- linhai-0.1.0/linhai/llm_manager.py +351 -0
- linhai-0.1.0/linhai/machine_control/__init__.py +15 -0
- linhai-0.1.0/linhai/machine_control/bash_host/__init__.py +5 -0
- linhai-0.1.0/linhai/machine_control/bash_host/bash_host.py +449 -0
- linhai-0.1.0/linhai/machine_control/bash_host/file.py +234 -0
- linhai-0.1.0/linhai/machine_control/bash_host/http.py +162 -0
- linhai-0.1.0/linhai/machine_control/bash_host/process.py +139 -0
- linhai-0.1.0/linhai/machine_control/bash_host/terminal.py +200 -0
- linhai-0.1.0/linhai/machine_control/ether_ghost_host/__init__.py +5 -0
- linhai-0.1.0/linhai/machine_control/ether_ghost_host/ether_ghost_host.py +317 -0
- linhai-0.1.0/linhai/machine_control/http_message.py +127 -0
- linhai-0.1.0/linhai/machine_control/main.py +496 -0
- linhai-0.1.0/linhai/machine_control/master_host/__init__.py +49 -0
- linhai-0.1.0/linhai/machine_control/master_host/file.py +462 -0
- linhai-0.1.0/linhai/machine_control/master_host/http.py +53 -0
- linhai-0.1.0/linhai/machine_control/master_host/master_host.py +361 -0
- linhai-0.1.0/linhai/machine_control/master_host/process.py +363 -0
- linhai-0.1.0/linhai/machine_control/master_host/terminal.py +286 -0
- linhai-0.1.0/linhai/machine_control/master_host/tmux_terminal.py +145 -0
- linhai-0.1.0/linhai/machine_control/plugin.py +88 -0
- linhai-0.1.0/linhai/machine_control/posix_shell/__init__.py +5 -0
- linhai-0.1.0/linhai/machine_control/posix_shell/posix_shell_control.py +406 -0
- linhai-0.1.0/linhai/machine_control/posix_shell/process.py +92 -0
- linhai-0.1.0/linhai/machine_control/process.py +87 -0
- linhai-0.1.0/linhai/machine_control/protocol.py +106 -0
- linhai-0.1.0/linhai/machine_control/tools.py +1040 -0
- linhai-0.1.0/linhai/machine_control/trojan/__init__.py +1 -0
- linhai-0.1.0/linhai/machine_control/trojan/shell_transport.py +218 -0
- linhai-0.1.0/linhai/machine_control/trojan/transport.py +167 -0
- linhai-0.1.0/linhai/machine_control/trojan/trojan.py +828 -0
- linhai-0.1.0/linhai/main.py +263 -0
- linhai-0.1.0/linhai/markdown_parser.py +217 -0
- linhai-0.1.0/linhai/multimodal.py +341 -0
- linhai-0.1.0/linhai/parsed_message.py +397 -0
- linhai-0.1.0/linhai/plugin/__init__.py +123 -0
- linhai-0.1.0/linhai/plugin/afk_plugin.py +39 -0
- linhai-0.1.0/linhai/plugin/catgirl_tone.py +64 -0
- linhai-0.1.0/linhai/plugin/claw.py +144 -0
- linhai-0.1.0/linhai/plugin/command_hints.py +319 -0
- linhai-0.1.0/linhai/plugin/file_operations.py +535 -0
- linhai-0.1.0/linhai/plugin/file_permission_plugin.py +81 -0
- linhai-0.1.0/linhai/plugin/helpers.py +85 -0
- linhai-0.1.0/linhai/plugin/message_checkers.py +768 -0
- linhai-0.1.0/linhai/plugin/planning.py +484 -0
- linhai-0.1.0/linhai/plugin/python_chore.py +133 -0
- linhai-0.1.0/linhai/plugin/reminder.py +128 -0
- linhai-0.1.0/linhai/plugin/security_config.py +268 -0
- linhai-0.1.0/linhai/plugin/stdio_command_checker.py +193 -0
- linhai-0.1.0/linhai/plugin/sudo_stdio_checker.py +115 -0
- linhai-0.1.0/linhai/plugin/system_message_leaning.py +88 -0
- linhai-0.1.0/linhai/plugin/telegram.py +285 -0
- linhai-0.1.0/linhai/plugin/tool_call_managers.py +369 -0
- linhai-0.1.0/linhai/plugin/user_reminder.py +41 -0
- linhai-0.1.0/linhai/prompt.py +1954 -0
- linhai-0.1.0/linhai/registry.py +123 -0
- linhai-0.1.0/linhai/sandbox.py +130 -0
- linhai-0.1.0/linhai/secret.py +512 -0
- linhai-0.1.0/linhai/task_supervisor.py +106 -0
- linhai-0.1.0/linhai/telegram.py +228 -0
- linhai-0.1.0/linhai/token_manager.py +208 -0
- linhai-0.1.0/linhai/tool/__init__.py +35 -0
- linhai-0.1.0/linhai/tool/base.py +474 -0
- linhai-0.1.0/linhai/tool/general.py +221 -0
- linhai-0.1.0/linhai/tool/main.py +198 -0
- linhai-0.1.0/linhai/tool/mcp_connector.py +288 -0
- linhai-0.1.0/linhai/tool/mcp_server_example.py +42 -0
- linhai-0.1.0/linhai/tool/search.py +175 -0
- linhai-0.1.0/linhai/tui/__init__.py +23 -0
- linhai-0.1.0/linhai/tui/app.py +319 -0
- linhai-0.1.0/linhai/tui/components.py +1304 -0
- linhai-0.1.0/linhai/tui/context_tab.py +412 -0
- linhai-0.1.0/linhai/tui/messages_list.py +284 -0
- linhai-0.1.0/linhai/tui/planning_tab.py +83 -0
- linhai-0.1.0/linhai/tui/process_tab.py +208 -0
- linhai-0.1.0/linhai/type_hints.py +170 -0
- linhai-0.1.0/linhai/utils/__init__.py +0 -0
- linhai-0.1.0/linhai/utils/common.py +191 -0
- linhai-0.1.0/linhai/utils/i18n.py +11 -0
- linhai-0.1.0/linhai/utils/input_parser.py +42 -0
- linhai-0.1.0/linhai/utils/jsonpubsub.py +216 -0
- linhai-0.1.0/linhai/utils/pulse_encoding.py +137 -0
- linhai-0.1.0/linhai/utils/streamjson.py +397 -0
- linhai-0.1.0/linhai/utils/token_parser.py +156 -0
- linhai-0.1.0/linhai/utils/tokenizer.py +120 -0
- linhai-0.1.0/linhai/webui/__init__.py +6 -0
- linhai-0.1.0/linhai/webui/agent_manager.py +393 -0
- linhai-0.1.0/linhai/webui/app.py +66 -0
- linhai-0.1.0/linhai/webui/routes.py +295 -0
- linhai-0.1.0/linhai/webui/schemas.py +155 -0
- linhai-0.1.0/pyproject.toml +61 -0
- linhai-0.1.0/requirements.lock +72 -0
- linhai-0.1.0/test_webui_messages.py +389 -0
- linhai-0.1.0/tests/__init__.py +1 -0
- linhai-0.1.0/tests/machine_control/__init__.py +0 -0
- linhai-0.1.0/tests/machine_control/ether_ghost_host/test_ether_ghost_host.py +217 -0
- linhai-0.1.0/tests/machine_control/test_bash_file.py +378 -0
- linhai-0.1.0/tests/machine_control/test_bash_host.py +453 -0
- linhai-0.1.0/tests/machine_control/test_bash_http.py +175 -0
- linhai-0.1.0/tests/machine_control/test_bash_terminal.py +219 -0
- linhai-0.1.0/tests/machine_control/test_env_parameter.py +182 -0
- linhai-0.1.0/tests/machine_control/test_heartbeat.py +253 -0
- linhai-0.1.0/tests/machine_control/test_http_message.py +251 -0
- linhai-0.1.0/tests/machine_control/test_local_process.py +122 -0
- linhai-0.1.0/tests/machine_control/test_posix_shell.py +94 -0
- linhai-0.1.0/tests/machine_control/test_pty_parameter.py +19 -0
- linhai-0.1.0/tests/machine_control/test_trojan_pty_e2e.py +119 -0
- linhai-0.1.0/tests/machine_control/trojan/test_shell_transport.py +295 -0
- linhai-0.1.0/tests/real_mcp_server.py +44 -0
- linhai-0.1.0/tests/test_afk_param.py +211 -0
- linhai-0.1.0/tests/test_agent.py +490 -0
- linhai-0.1.0/tests/test_agent_answer.py +157 -0
- linhai-0.1.0/tests/test_agent_at_system.py +178 -0
- linhai-0.1.0/tests/test_agent_build_context.py +387 -0
- linhai-0.1.0/tests/test_agent_global_memory.py +75 -0
- linhai-0.1.0/tests/test_agent_lifecycle.py +313 -0
- linhai-0.1.0/tests/test_agent_main.py +60 -0
- linhai-0.1.0/tests/test_agent_marker.py +376 -0
- linhai-0.1.0/tests/test_agent_message.py +197 -0
- linhai-0.1.0/tests/test_agent_orchestration.py +886 -0
- linhai-0.1.0/tests/test_agent_plugin.py +958 -0
- linhai-0.1.0/tests/test_agent_workflow.py +604 -0
- linhai-0.1.0/tests/test_answer_truncate.py +159 -0
- linhai-0.1.0/tests/test_binary.zip +0 -0
- linhai-0.1.0/tests/test_catgirl_tone.py +113 -0
- linhai-0.1.0/tests/test_claw.py +175 -0
- linhai-0.1.0/tests/test_claw_heartbeat.py +133 -0
- linhai-0.1.0/tests/test_command_completion.py +148 -0
- linhai-0.1.0/tests/test_command_handler.py +226 -0
- linhai-0.1.0/tests/test_command_whitelist.py +168 -0
- linhai-0.1.0/tests/test_config.py +722 -0
- linhai-0.1.0/tests/test_config.toml +18 -0
- linhai-0.1.0/tests/test_config_mcp.py +144 -0
- linhai-0.1.0/tests/test_connect_posix_shell_as_machine.py +413 -0
- linhai-0.1.0/tests/test_context_tab.py +694 -0
- linhai-0.1.0/tests/test_conversation_save.py +129 -0
- linhai-0.1.0/tests/test_conversation_system.py +160 -0
- linhai-0.1.0/tests/test_create.py +694 -0
- linhai-0.1.0/tests/test_create_agent.py +142 -0
- linhai-0.1.0/tests/test_create_agent_mcp.py +175 -0
- linhai-0.1.0/tests/test_create_command_whitelist.py +164 -0
- linhai-0.1.0/tests/test_create_integration.py +74 -0
- linhai-0.1.0/tests/test_cron.py +256 -0
- linhai-0.1.0/tests/test_current_directory.py +47 -0
- linhai-0.1.0/tests/test_dummy_tools_migration.py +86 -0
- linhai-0.1.0/tests/test_dynamic_connection_restore.py +251 -0
- linhai-0.1.0/tests/test_dynamic_file_content_message.py +104 -0
- linhai-0.1.0/tests/test_end_think_plugin.py +96 -0
- linhai-0.1.0/tests/test_enter_key_clear_input.py +203 -0
- linhai-0.1.0/tests/test_exit_tool_functionality.py +85 -0
- linhai-0.1.0/tests/test_explicit_cache_config.py +218 -0
- linhai-0.1.0/tests/test_extract_usage.py +147 -0
- linhai-0.1.0/tests/test_file_permission_plugin.py +144 -0
- linhai-0.1.0/tests/test_file_read_write_conflict_plugin.py +330 -0
- linhai-0.1.0/tests/test_file_tools.py +285 -0
- linhai-0.1.0/tests/test_footer_widget.py +91 -0
- linhai-0.1.0/tests/test_glm_insult_mask_plugin.py +210 -0
- linhai-0.1.0/tests/test_glm_thinking.py +77 -0
- linhai-0.1.0/tests/test_glm_tool_call_plugin.py +114 -0
- linhai-0.1.0/tests/test_global_memory_config.py +255 -0
- linhai-0.1.0/tests/test_helpers.py +88 -0
- linhai-0.1.0/tests/test_http_request.py +421 -0
- linhai-0.1.0/tests/test_i18n.py +444 -0
- linhai-0.1.0/tests/test_init_app_textual.py +92 -0
- linhai-0.1.0/tests/test_init_config_writer.py +230 -0
- linhai-0.1.0/tests/test_input_parser.py +75 -0
- linhai-0.1.0/tests/test_issue_10_multiple_messages.py +142 -0
- linhai-0.1.0/tests/test_json_serialization.py +103 -0
- linhai-0.1.0/tests/test_jsonpubsub_generation.py +114 -0
- linhai-0.1.0/tests/test_kimi_k25_tool_call_plugin.py +77 -0
- linhai-0.1.0/tests/test_large_message_marking.py +138 -0
- linhai-0.1.0/tests/test_lifecycle_callbacks.py +94 -0
- linhai-0.1.0/tests/test_llm_manager.py +510 -0
- linhai-0.1.0/tests/test_llm_switching.py +186 -0
- linhai-0.1.0/tests/test_llm_token_usage.py +206 -0
- linhai-0.1.0/tests/test_machine_control.py +1129 -0
- linhai-0.1.0/tests/test_machine_control_task_supervisor.py +224 -0
- linhai-0.1.0/tests/test_main.py +353 -0
- linhai-0.1.0/tests/test_markdown_parser.py +136 -0
- linhai-0.1.0/tests/test_master_host_sandbox.py +116 -0
- linhai-0.1.0/tests/test_max_toolcall_token_in_round_float.py +117 -0
- linhai-0.1.0/tests/test_mcp_real_server.py +105 -0
- linhai-0.1.0/tests/test_message_collapse.py +375 -0
- linhai-0.1.0/tests/test_messages_list_serialize.py +274 -0
- linhai-0.1.0/tests/test_minimax_tool_call_plugin.py +107 -0
- linhai-0.1.0/tests/test_misplaced_tool_call_plugin.py +97 -0
- linhai-0.1.0/tests/test_missing_with_secret_warning.py +165 -0
- linhai-0.1.0/tests/test_multimodal.py +312 -0
- linhai-0.1.0/tests/test_multimodal_manager.py +169 -0
- linhai-0.1.0/tests/test_normal_content_widget.py +175 -0
- linhai-0.1.0/tests/test_notification_message_plugin.py +60 -0
- linhai-0.1.0/tests/test_only_reasoning_plugin.py +254 -0
- linhai-0.1.0/tests/test_openai_timeout_config.py +69 -0
- linhai-0.1.0/tests/test_openai_toolcall_execution.py +182 -0
- linhai-0.1.0/tests/test_openai_toolcall_feeder.py +138 -0
- linhai-0.1.0/tests/test_openai_toolcalls.py +237 -0
- linhai-0.1.0/tests/test_override_toolsets.py +297 -0
- linhai-0.1.0/tests/test_parsed_message.py +510 -0
- linhai-0.1.0/tests/test_pinned_messages.py +391 -0
- linhai-0.1.0/tests/test_pkill_checker.py +90 -0
- linhai-0.1.0/tests/test_planning_integration.py +349 -0
- linhai-0.1.0/tests/test_planning_message.py +57 -0
- linhai-0.1.0/tests/test_planning_plugins.py +774 -0
- linhai-0.1.0/tests/test_planning_tab.py +358 -0
- linhai-0.1.0/tests/test_posix_shell_terminal.py +330 -0
- linhai-0.1.0/tests/test_process_argv_checker.py +195 -0
- linhai-0.1.0/tests/test_process_tab.py +603 -0
- linhai-0.1.0/tests/test_prompt_fast_agent_plugin.py +209 -0
- linhai-0.1.0/tests/test_python_chore_plugin.py +316 -0
- linhai-0.1.0/tests/test_queue_interrupt.py +141 -0
- linhai-0.1.0/tests/test_rainbow_ascii_art.py +133 -0
- linhai-0.1.0/tests/test_reasoning_content_widget.py +374 -0
- linhai-0.1.0/tests/test_registry_cleanup.py +69 -0
- linhai-0.1.0/tests/test_reminder.py +252 -0
- linhai-0.1.0/tests/test_runtime_imitation_plugin.py +150 -0
- linhai-0.1.0/tests/test_sandbox.py +313 -0
- linhai-0.1.0/tests/test_secret.py +784 -0
- linhai-0.1.0/tests/test_secret_call_with_secret.py +293 -0
- linhai-0.1.0/tests/test_secret_exception_leak.py +295 -0
- linhai-0.1.0/tests/test_secret_initialize.py +64 -0
- linhai-0.1.0/tests/test_secret_intercept_file.py +197 -0
- linhai-0.1.0/tests/test_sed_fragmented_read_plugin.py +382 -0
- linhai-0.1.0/tests/test_serialize_restore_system_agent.py +165 -0
- linhai-0.1.0/tests/test_sleeping_state.py +275 -0
- linhai-0.1.0/tests/test_split_and_save_large_output.py +116 -0
- linhai-0.1.0/tests/test_state_machine.py +153 -0
- linhai-0.1.0/tests/test_stdio_command_checker.py +172 -0
- linhai-0.1.0/tests/test_streamjson.py +161 -0
- linhai-0.1.0/tests/test_sudo_stdio_checker.py +121 -0
- linhai-0.1.0/tests/test_system_message.py +187 -0
- linhai-0.1.0/tests/test_system_message_leaning.py +59 -0
- linhai-0.1.0/tests/test_task_supervisor.py +166 -0
- linhai-0.1.0/tests/test_telegram_plugin.py +400 -0
- linhai-0.1.0/tests/test_telegram_sticker_message.py +329 -0
- linhai-0.1.0/tests/test_terminal_backend.py +98 -0
- linhai-0.1.0/tests/test_terminal_toolset.py +80 -0
- linhai-0.1.0/tests/test_tmux_terminal.py +146 -0
- linhai-0.1.0/tests/test_todolist_checker_plugin.py +229 -0
- linhai-0.1.0/tests/test_token_cache.py +128 -0
- linhai-0.1.0/tests/test_token_manager.py +62 -0
- linhai-0.1.0/tests/test_token_usage_integration.py +153 -0
- linhai-0.1.0/tests/test_tokenizer.py +59 -0
- linhai-0.1.0/tests/test_tool_call_in_reasoning_plugin.py +198 -0
- linhai-0.1.0/tests/test_tool_call_managers.py +138 -0
- linhai-0.1.0/tests/test_tool_call_widget.py +235 -0
- linhai-0.1.0/tests/test_toolcall.py +332 -0
- linhai-0.1.0/tests/test_toolcall_collapse.py +259 -0
- linhai-0.1.0/tests/test_toolcall_error.py +56 -0
- linhai-0.1.0/tests/test_toolcall_token_management.py +335 -0
- linhai-0.1.0/tests/test_tui_tabs.py +144 -0
- linhai-0.1.0/tests/test_tui_theme.py +123 -0
- linhai-0.1.0/tests/test_two_step_compression.py +539 -0
- linhai-0.1.0/tests/test_unnecessary_run_command_plugin.py +305 -0
- linhai-0.1.0/tests/test_unnecessary_sed_read_plugin.py +1128 -0
- linhai-0.1.0/tests/test_user_plugins.py +163 -0
- linhai-0.1.0/tests/test_user_reminder.py +122 -0
- linhai-0.1.0/tests/test_utils.py +150 -0
- linhai-0.1.0/tests/test_volcano_deepseek_fix.py +112 -0
- linhai-0.1.0/tests/test_volcano_deepseek_fix_plugin.py +165 -0
- linhai-0.1.0/tests/test_waiting_user_reminder.py +98 -0
- linhai-0.1.0/tests/test_webui.py +722 -0
- linhai-0.1.0/tests/test_workflow_message_prepare.py +220 -0
- linhai-0.1.0/tests/tool/test_file_validation.py +156 -0
- linhai-0.1.0/tests/tool/test_http_request.py +369 -0
- linhai-0.1.0/tests/tool/test_http_tools.py +282 -0
- linhai-0.1.0/tests/tool/test_json_schema.py +198 -0
- linhai-0.1.0/tests/tool/test_mcp_connector.py +327 -0
- linhai-0.1.0/tests/tool/test_process_tools.py +186 -0
- linhai-0.1.0/tests/tool/test_terminal.py +118 -0
- linhai-0.1.0/tests/tool/test_tool_functions.py +62 -0
- linhai-0.1.0/tests/tool/test_tool_i18n.py +129 -0
- linhai-0.1.0/tests/tool/test_tool_manager.py +251 -0
- linhai-0.1.0/tests/tool/test_tool_result_message.py +162 -0
- linhai-0.1.0/uv.lock +2235 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import ast
|
|
5
|
+
from typing import List, Set, Tuple, Optional, Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_any_aliases(tree: ast.AST) -> Set[str]:
|
|
9
|
+
"""Extract all aliases for typing.Any from import statements."""
|
|
10
|
+
aliases: Set[str] = {"Any"}
|
|
11
|
+
typing_module_names: Set[str] = set()
|
|
12
|
+
|
|
13
|
+
for node in ast.walk(tree):
|
|
14
|
+
if isinstance(node, ast.ImportFrom):
|
|
15
|
+
if node.module == "typing":
|
|
16
|
+
for alias in node.names:
|
|
17
|
+
if alias.name == "Any":
|
|
18
|
+
if alias.asname:
|
|
19
|
+
aliases.add(alias.asname)
|
|
20
|
+
elif node.module and node.module.startswith("typing."):
|
|
21
|
+
for alias in node.names:
|
|
22
|
+
if alias.name == "Any":
|
|
23
|
+
if alias.asname:
|
|
24
|
+
aliases.add(alias.asname)
|
|
25
|
+
elif isinstance(node, ast.Import):
|
|
26
|
+
for alias in node.names:
|
|
27
|
+
if alias.name == "typing" or alias.name.startswith("typing."):
|
|
28
|
+
typing_module_names.add(alias.asname or alias.name)
|
|
29
|
+
print(aliases)
|
|
30
|
+
return aliases
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_changed_python_files(base_ref: str = "origin/main") -> List[str]:
|
|
34
|
+
try:
|
|
35
|
+
check_cmd = ["git", "rev-parse", "--verify", base_ref]
|
|
36
|
+
if subprocess.run(check_cmd, capture_output=True).returncode != 0:
|
|
37
|
+
base_ref = "HEAD^"
|
|
38
|
+
|
|
39
|
+
cmd = [
|
|
40
|
+
"git",
|
|
41
|
+
"diff",
|
|
42
|
+
"--name-only",
|
|
43
|
+
"--diff-filter=ACMR",
|
|
44
|
+
base_ref,
|
|
45
|
+
"HEAD",
|
|
46
|
+
"--",
|
|
47
|
+
"linhai/",
|
|
48
|
+
]
|
|
49
|
+
output = subprocess.check_output(
|
|
50
|
+
cmd, universal_newlines=True, stderr=subprocess.DEVNULL
|
|
51
|
+
).strip()
|
|
52
|
+
|
|
53
|
+
if not output:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
files = output.split("\n")
|
|
57
|
+
return [f for f in files if f.endswith(".py") and os.path.exists(f)]
|
|
58
|
+
except subprocess.CalledProcessError:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_any_or_object_annotation(
|
|
63
|
+
node: ast.AST, any_aliases: Set[str]
|
|
64
|
+
) -> Tuple[bool, str]:
|
|
65
|
+
if isinstance(node, ast.Name):
|
|
66
|
+
if node.id in any_aliases:
|
|
67
|
+
if node.id == "Any":
|
|
68
|
+
return True, "Any"
|
|
69
|
+
else:
|
|
70
|
+
return True, f"Any (imported as '{node.id}')"
|
|
71
|
+
if node.id == "object":
|
|
72
|
+
return True, "object"
|
|
73
|
+
elif isinstance(node, ast.Attribute):
|
|
74
|
+
if (
|
|
75
|
+
node.attr == "Any"
|
|
76
|
+
and isinstance(node.value, ast.Name)
|
|
77
|
+
and node.value.id == "typing"
|
|
78
|
+
):
|
|
79
|
+
return True, "typing.Any"
|
|
80
|
+
if (
|
|
81
|
+
node.attr == "Object"
|
|
82
|
+
and isinstance(node.value, ast.Name)
|
|
83
|
+
and node.value.id == "typing"
|
|
84
|
+
):
|
|
85
|
+
return True, "typing.Object"
|
|
86
|
+
return False, ""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_function_args(
|
|
90
|
+
node: ast.FunctionDef, violations: List[Tuple[int, str, str]], any_aliases: Set[str]
|
|
91
|
+
) -> None:
|
|
92
|
+
for arg in node.args.args:
|
|
93
|
+
if arg.annotation:
|
|
94
|
+
is_invalid, type_name = is_any_or_object_annotation(
|
|
95
|
+
arg.annotation, any_aliases
|
|
96
|
+
)
|
|
97
|
+
if is_invalid:
|
|
98
|
+
violations.append((arg.lineno, arg.arg, type_name))
|
|
99
|
+
|
|
100
|
+
for arg in node.args.kwonlyargs:
|
|
101
|
+
if arg.annotation:
|
|
102
|
+
is_invalid, type_name = is_any_or_object_annotation(
|
|
103
|
+
arg.annotation, any_aliases
|
|
104
|
+
)
|
|
105
|
+
if is_invalid:
|
|
106
|
+
violations.append((arg.lineno, arg.arg, type_name))
|
|
107
|
+
|
|
108
|
+
if node.args.vararg and node.args.vararg.annotation:
|
|
109
|
+
is_invalid, type_name = is_any_or_object_annotation(
|
|
110
|
+
node.args.vararg.annotation, any_aliases
|
|
111
|
+
)
|
|
112
|
+
if is_invalid:
|
|
113
|
+
violations.append(
|
|
114
|
+
(node.args.vararg.lineno, node.args.vararg.arg, type_name)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if node.args.kwarg and node.args.kwarg.annotation:
|
|
118
|
+
is_invalid, type_name = is_any_or_object_annotation(
|
|
119
|
+
node.args.kwarg.annotation, any_aliases
|
|
120
|
+
)
|
|
121
|
+
if is_invalid:
|
|
122
|
+
violations.append((node.args.kwarg.lineno, node.args.kwarg.arg, type_name))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_any_object_in_file(file_path: str) -> List[Tuple[int, str, str]]:
|
|
126
|
+
violations = []
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
130
|
+
content = f.read()
|
|
131
|
+
tree = ast.parse(content, filename=file_path)
|
|
132
|
+
except (SyntaxError, UnicodeDecodeError) as e:
|
|
133
|
+
print(f"WARNING: Failed to parse {file_path}: {e}")
|
|
134
|
+
return violations
|
|
135
|
+
|
|
136
|
+
any_aliases = get_any_aliases(tree)
|
|
137
|
+
|
|
138
|
+
for node in ast.walk(tree):
|
|
139
|
+
if isinstance(node, ast.FunctionDef):
|
|
140
|
+
check_function_args(node, violations, any_aliases)
|
|
141
|
+
elif isinstance(node, ast.AsyncFunctionDef):
|
|
142
|
+
check_function_args(node, violations, any_aliases)
|
|
143
|
+
|
|
144
|
+
return violations
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main() -> None:
|
|
148
|
+
base_ref = os.environ.get("BASE_REF")
|
|
149
|
+
if base_ref:
|
|
150
|
+
base_ref = f"origin/{base_ref}"
|
|
151
|
+
else:
|
|
152
|
+
base_ref = "origin/main"
|
|
153
|
+
|
|
154
|
+
python_files = get_changed_python_files(base_ref)
|
|
155
|
+
|
|
156
|
+
if not python_files:
|
|
157
|
+
print("No Python files changed in linhai/ directory.")
|
|
158
|
+
sys.exit(0)
|
|
159
|
+
|
|
160
|
+
print(
|
|
161
|
+
f"Checking {len(python_files)} changed Python file(s) for Any/object type annotations..."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
errors_found = False
|
|
165
|
+
for file_path in python_files:
|
|
166
|
+
violations = check_any_object_in_file(file_path)
|
|
167
|
+
if violations:
|
|
168
|
+
errors_found = True
|
|
169
|
+
print(f"\nERROR: Any/object type annotations found in {file_path}:")
|
|
170
|
+
for line, arg_name, type_name in violations:
|
|
171
|
+
print(
|
|
172
|
+
f" Line {line}: Parameter '{arg_name}' has type annotation '{type_name}'"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if errors_found:
|
|
176
|
+
print(
|
|
177
|
+
"\nERROR: Any or object type annotations detected in function parameters."
|
|
178
|
+
)
|
|
179
|
+
print("Using type aliases (e.g., 'from typing import Any as XXX') to bypass")
|
|
180
|
+
print("detection is FORBIDDEN and violates the code standards.")
|
|
181
|
+
print("\nPlease use specific types instead of Any or object.")
|
|
182
|
+
print("Examples:")
|
|
183
|
+
print(" def foo(x: int) -> str: # Good")
|
|
184
|
+
print(" def bar(x: Optional[int]) -> None: # Good")
|
|
185
|
+
print(" def baz(x: Any) -> None: # BAD")
|
|
186
|
+
print(" def qux(x: XXX) -> None: # BAD (where XXX is an alias for Any)")
|
|
187
|
+
sys.exit(1)
|
|
188
|
+
else:
|
|
189
|
+
print("SUCCESS: No Any/object type annotations found in changed Python files.")
|
|
190
|
+
sys.exit(0)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_changed_py_files(base_ref="origin/main"):
|
|
8
|
+
try:
|
|
9
|
+
check_cmd = ["git", "rev-parse", "--verify", base_ref]
|
|
10
|
+
if subprocess.run(check_cmd, capture_output=True).returncode != 0:
|
|
11
|
+
base_ref = "HEAD^"
|
|
12
|
+
cmd = ["git", "diff", "--name-only", base_ref, "HEAD", "--", "linhai/"]
|
|
13
|
+
output = subprocess.check_output(
|
|
14
|
+
cmd, universal_newlines=True, stderr=subprocess.DEVNULL
|
|
15
|
+
)
|
|
16
|
+
files = [f for f in output.strip().split("\n") if f and f.endswith(".py")]
|
|
17
|
+
return files
|
|
18
|
+
except subprocess.CalledProcessError:
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_black_overformat(file_path):
|
|
23
|
+
try:
|
|
24
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
25
|
+
original = f.read()
|
|
26
|
+
|
|
27
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tmp:
|
|
28
|
+
tmp.write(original)
|
|
29
|
+
tmp_path = tmp.name
|
|
30
|
+
|
|
31
|
+
cmd = ["uv", "run", "black", "--quiet", tmp_path]
|
|
32
|
+
subprocess.run(cmd, capture_output=True, check=True)
|
|
33
|
+
|
|
34
|
+
with open(tmp_path, "r", encoding="utf-8") as f:
|
|
35
|
+
formatted = f.read()
|
|
36
|
+
|
|
37
|
+
os.unlink(tmp_path)
|
|
38
|
+
|
|
39
|
+
if original != formatted:
|
|
40
|
+
original_lines = original.split("\n")
|
|
41
|
+
formatted_lines = formatted.split("\n")
|
|
42
|
+
|
|
43
|
+
diff_count = 0
|
|
44
|
+
for i, (orig, fmt) in enumerate(zip(original_lines, formatted_lines)):
|
|
45
|
+
if orig != fmt:
|
|
46
|
+
stripped_orig = orig.rstrip()
|
|
47
|
+
stripped_fmt = fmt.rstrip()
|
|
48
|
+
if stripped_orig == stripped_fmt:
|
|
49
|
+
diff_count += 1
|
|
50
|
+
|
|
51
|
+
if diff_count > len(original_lines) * 0.3:
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
except Exception:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main():
|
|
59
|
+
base_ref = "origin/main"
|
|
60
|
+
try:
|
|
61
|
+
check_cmd = ["git", "rev-parse", "--verify", base_ref]
|
|
62
|
+
if subprocess.run(check_cmd, capture_output=True).returncode != 0:
|
|
63
|
+
base_ref = "HEAD^"
|
|
64
|
+
except Exception:
|
|
65
|
+
base_ref = "HEAD^"
|
|
66
|
+
|
|
67
|
+
files = get_changed_py_files(base_ref)
|
|
68
|
+
if not files:
|
|
69
|
+
print("No Python files changed in linhai/ directory.")
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
|
|
72
|
+
errors = []
|
|
73
|
+
for file_path in files:
|
|
74
|
+
if check_black_overformat(file_path):
|
|
75
|
+
errors.append(file_path)
|
|
76
|
+
|
|
77
|
+
if errors:
|
|
78
|
+
print("ERROR: Black appears to be overformatting these files:")
|
|
79
|
+
for err in errors:
|
|
80
|
+
print(f" {err}")
|
|
81
|
+
print("\nPlease check if black is making excessive formatting changes.")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
else:
|
|
84
|
+
print("SUCCESS: No black overformatting detected.")
|
|
85
|
+
sys.exit(0)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [ -z "${CI:-}" ]; then
|
|
5
|
+
echo "Not running in CI, skipping branch behind check."
|
|
6
|
+
exit 0
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
if git merge-base --is-ancestor origin/main HEAD 2>/dev/null; then
|
|
10
|
+
echo "Branch is up to date with main."
|
|
11
|
+
else
|
|
12
|
+
echo "ERROR: Branch is behind or diverged from main."
|
|
13
|
+
echo "Please rebase your branch onto main: git fetch upstream && git rebase upstream/main"
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import ast
|
|
5
|
+
from typing import List, Set, Tuple, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_changed_python_files(base_ref: str = "origin/main") -> List[str]:
|
|
9
|
+
"""
|
|
10
|
+
Get list of changed Python files in the diff.
|
|
11
|
+
"""
|
|
12
|
+
try:
|
|
13
|
+
# Check if base_ref exists, fallback to HEAD^
|
|
14
|
+
check_cmd = ["git", "rev-parse", "--verify", base_ref]
|
|
15
|
+
if subprocess.run(check_cmd, capture_output=True).returncode != 0:
|
|
16
|
+
base_ref = "HEAD^"
|
|
17
|
+
|
|
18
|
+
cmd = [
|
|
19
|
+
"git",
|
|
20
|
+
"diff",
|
|
21
|
+
"--name-only",
|
|
22
|
+
"--diff-filter=d",
|
|
23
|
+
base_ref,
|
|
24
|
+
"HEAD",
|
|
25
|
+
"--",
|
|
26
|
+
"linhai/",
|
|
27
|
+
]
|
|
28
|
+
output = subprocess.check_output(
|
|
29
|
+
cmd, universal_newlines=True, stderr=subprocess.DEVNULL
|
|
30
|
+
).strip()
|
|
31
|
+
|
|
32
|
+
if not output:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
files = output.split("\n")
|
|
36
|
+
return [f for f in files if f.endswith(".py") and os.path.exists(f)]
|
|
37
|
+
except subprocess.CalledProcessError:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_import_aliases(tree: ast.AST) -> Set[str]:
|
|
42
|
+
"""
|
|
43
|
+
Extract all import aliases for 'cast' from the AST.
|
|
44
|
+
Returns set of names that refer to cast function.
|
|
45
|
+
"""
|
|
46
|
+
cast_names = {"cast"} # Always include 'cast' as a base name
|
|
47
|
+
|
|
48
|
+
for node in ast.walk(tree):
|
|
49
|
+
if isinstance(node, ast.ImportFrom):
|
|
50
|
+
if node.module == "typing" or (
|
|
51
|
+
node.module and node.module.endswith(".typing")
|
|
52
|
+
):
|
|
53
|
+
for alias in node.names:
|
|
54
|
+
if alias.name == "cast":
|
|
55
|
+
if alias.asname:
|
|
56
|
+
cast_names.add(alias.asname)
|
|
57
|
+
else:
|
|
58
|
+
cast_names.add("cast")
|
|
59
|
+
elif isinstance(node, ast.Import):
|
|
60
|
+
for alias in node.names:
|
|
61
|
+
if alias.name == "typing":
|
|
62
|
+
# import typing - we'll need to check typing.cast
|
|
63
|
+
pass
|
|
64
|
+
elif alias.name == "cast":
|
|
65
|
+
if alias.asname:
|
|
66
|
+
cast_names.add(alias.asname)
|
|
67
|
+
else:
|
|
68
|
+
cast_names.add("cast")
|
|
69
|
+
|
|
70
|
+
return cast_names
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def check_cast_usage_in_file(file_path: str) -> List[Tuple[int, int, str]]:
|
|
74
|
+
"""
|
|
75
|
+
Check a Python file for cast usage.
|
|
76
|
+
Returns list of violations as (line, col, message).
|
|
77
|
+
"""
|
|
78
|
+
violations = []
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
82
|
+
content = f.read()
|
|
83
|
+
tree = ast.parse(content, filename=file_path)
|
|
84
|
+
except (SyntaxError, UnicodeDecodeError) as e:
|
|
85
|
+
print(f"WARNING: Failed to parse {file_path}: {e}")
|
|
86
|
+
return violations
|
|
87
|
+
|
|
88
|
+
# Get all names that refer to cast function
|
|
89
|
+
cast_names = get_import_aliases(tree)
|
|
90
|
+
|
|
91
|
+
# Walk AST to find cast calls
|
|
92
|
+
for node in ast.walk(tree):
|
|
93
|
+
if isinstance(node, ast.Call):
|
|
94
|
+
# Check for direct cast call: cast(Type, value)
|
|
95
|
+
if isinstance(node.func, ast.Name):
|
|
96
|
+
if node.func.id in cast_names:
|
|
97
|
+
violations.append(
|
|
98
|
+
(
|
|
99
|
+
node.lineno,
|
|
100
|
+
node.col_offset,
|
|
101
|
+
f"Direct cast call: {node.func.id}",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Check for typing.cast call
|
|
106
|
+
elif isinstance(node.func, ast.Attribute):
|
|
107
|
+
if node.func.attr == "cast":
|
|
108
|
+
# Check if it's typing.cast or something.typing.cast
|
|
109
|
+
if isinstance(node.func.value, ast.Name):
|
|
110
|
+
if node.func.value.id == "typing":
|
|
111
|
+
violations.append(
|
|
112
|
+
(node.lineno, node.col_offset, "typing.cast call")
|
|
113
|
+
)
|
|
114
|
+
elif isinstance(node.func.value, ast.Attribute):
|
|
115
|
+
# Handle cases like module.typing.cast
|
|
116
|
+
if isinstance(node.func.value.value, ast.Name):
|
|
117
|
+
if (
|
|
118
|
+
node.func.value.value.id + "." + node.func.value.attr
|
|
119
|
+
== "typing"
|
|
120
|
+
):
|
|
121
|
+
violations.append(
|
|
122
|
+
(node.lineno, node.col_offset, "typing.cast call")
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return violations
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def main() -> None:
|
|
129
|
+
"""
|
|
130
|
+
Main function to check all changed Python files for cast usage.
|
|
131
|
+
"""
|
|
132
|
+
base_ref = os.environ.get("BASE_REF")
|
|
133
|
+
if base_ref:
|
|
134
|
+
base_ref = f"origin/{base_ref}"
|
|
135
|
+
else:
|
|
136
|
+
base_ref = "origin/main"
|
|
137
|
+
|
|
138
|
+
python_files = get_changed_python_files(base_ref)
|
|
139
|
+
|
|
140
|
+
if not python_files:
|
|
141
|
+
print("No Python files changed in linhai/ directory.")
|
|
142
|
+
sys.exit(0)
|
|
143
|
+
|
|
144
|
+
print(f"Checking {len(python_files)} changed Python file(s) for cast usage...")
|
|
145
|
+
|
|
146
|
+
errors_found = False
|
|
147
|
+
for file_path in python_files:
|
|
148
|
+
violations = check_cast_usage_in_file(file_path)
|
|
149
|
+
if violations:
|
|
150
|
+
errors_found = True
|
|
151
|
+
print(f"\nERROR: Cast usage found in {file_path}:")
|
|
152
|
+
for line, col, msg in violations:
|
|
153
|
+
print(f" Line {line}, Col {col}: {msg}")
|
|
154
|
+
|
|
155
|
+
if errors_found:
|
|
156
|
+
print("\nERROR: cast() or typing.cast() usage detected.")
|
|
157
|
+
print(
|
|
158
|
+
"Please avoid using cast for type assertions. Use proper type hints instead."
|
|
159
|
+
)
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
else:
|
|
162
|
+
print("SUCCESS: No cast usage found in changed Python files.")
|
|
163
|
+
sys.exit(0)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def contains_emoji(text):
|
|
8
|
+
emoji_pattern = re.compile(
|
|
9
|
+
"["
|
|
10
|
+
"\U0001f600-\U0001f64f"
|
|
11
|
+
"\U0001f300 \U0001f5ff"
|
|
12
|
+
"\U0001f680-\U0001f6ff"
|
|
13
|
+
"\U0001f1e0-\U0001f1ff"
|
|
14
|
+
"]",
|
|
15
|
+
flags=re.UNICODE,
|
|
16
|
+
)
|
|
17
|
+
return bool(emoji_pattern.search(text))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_changed_files():
|
|
21
|
+
current_branch = os.environ.get("GITHUB_REF_NAME", "")
|
|
22
|
+
target_branch = os.environ.get("GITHUB_BASE_REF", "main")
|
|
23
|
+
if not current_branch:
|
|
24
|
+
try:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
)
|
|
30
|
+
current_branch = result.stdout.strip()
|
|
31
|
+
except:
|
|
32
|
+
current_branch = "HEAD"
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["git", "diff", "--name-only", f"{target_branch}...{current_branch}", "--"],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
)
|
|
39
|
+
if result.returncode != 0:
|
|
40
|
+
print(f"Warning: git diff failed: {result.stderr}")
|
|
41
|
+
return []
|
|
42
|
+
files = result.stdout.strip().split("\n")
|
|
43
|
+
python_files = [f for f in files if f and f.endswith(".py")]
|
|
44
|
+
return python_files
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"Warning: Could not get changed files: {e}")
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_changed_files():
|
|
51
|
+
changed_files = get_changed_files()
|
|
52
|
+
if not changed_files:
|
|
53
|
+
print("No Python files changed in this PR.")
|
|
54
|
+
return []
|
|
55
|
+
errors = []
|
|
56
|
+
for filepath in changed_files:
|
|
57
|
+
if not os.path.exists(filepath):
|
|
58
|
+
print(f"Warning: File {filepath} does not exist.")
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
62
|
+
content = f.read()
|
|
63
|
+
if contains_emoji(content):
|
|
64
|
+
errors.append(filepath)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"Warning: Could not read {filepath}: {e}")
|
|
67
|
+
return errors
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main():
|
|
71
|
+
print("Checking for emoji in changed Python files...")
|
|
72
|
+
errors = check_changed_files()
|
|
73
|
+
if errors:
|
|
74
|
+
print("ERROR: Emoji detected in changed code files:")
|
|
75
|
+
for err in errors:
|
|
76
|
+
print(f" {err}")
|
|
77
|
+
print("\nPlease remove emoji from changed code files.")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
else:
|
|
80
|
+
print("SUCCESS: No emoji found in changed code files.")
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# This script checks if the number of commits in a PR is reasonable.
|
|
4
|
+
# It fails if:
|
|
5
|
+
# - number of commits > number of files changed
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
echo "Checking if commit count is reasonable..."
|
|
10
|
+
|
|
11
|
+
# Check if we're in a PR context
|
|
12
|
+
if ! git rev-parse --verify origin/main >/dev/null 2>&1; then
|
|
13
|
+
echo "WARNING: Cannot find origin/main reference, skipping commit count check"
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Get the number of commits in the PR (from origin/main to HEAD)
|
|
18
|
+
commit_count=$(git log --oneline origin/main..HEAD | wc -l)
|
|
19
|
+
|
|
20
|
+
# Get the number of files changed (excluding deleted files)
|
|
21
|
+
file_count=$(git diff --name-only --diff-filter=ACMR origin/main..HEAD | wc -l)
|
|
22
|
+
|
|
23
|
+
echo "Number of commits: $commit_count"
|
|
24
|
+
echo "Number of files changed: $file_count"
|
|
25
|
+
|
|
26
|
+
# If commit count exceeds file count, fail
|
|
27
|
+
if [ "$commit_count" -gt "$file_count" ]; then
|
|
28
|
+
echo "ERROR: PR has $commit_count commits but only $file_count files changed."
|
|
29
|
+
echo " Please squash commits to have at most one commit per file changed."
|
|
30
|
+
exit 1
|
|
31
|
+
else
|
|
32
|
+
echo "SUCCESS: Commit count check passed."
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|