drydock-cli 0.6.4__tar.gz → 0.6.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.
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/PKG-INFO +1 -1
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/pyproject.toml +1 -1
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/scripts/deploy_to_github.sh +1 -1
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/scripts/publish_to_pypi.sh +1 -1
- drydock_cli-0.6.6/tests/test_agent_tasks.py +238 -0
- drydock_cli-0.6.6/tests/test_loop_detection.py +398 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.github/CODEOWNERS +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.github/DISCUSSION_TEMPLATE/ideas.yml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.github/workflows/build-and-upload.yml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.github/workflows/ci.yml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.github/workflows/issue-labeler.yml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.github/workflows/release.yml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.gitignore +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.pre-commit-config.yaml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.python-version +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.typos.toml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.vscode/extensions.json +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.vscode/launch.json +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/.vscode/settings.json +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/AGENTS.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/CHANGELOG.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/CLAUDE.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/CONTRIBUTING.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/Drydock_rebrand.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/LICENSE +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/NOTICE +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/PRD.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/README.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/action.yml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/distribution/zed/LICENSE +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/distribution/zed/extension.toml +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/distribution/zed/icons/mistral_vibe.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/docs/README.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/docs/acp-setup.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/docs/proxy-setup.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/acp_agent_loop.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/acp_logger.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/entrypoint.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/tools/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/tools/base.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/tools/builtins/bash.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/tools/builtins/read_file.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/tools/builtins/search_replace.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/tools/builtins/todo.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/tools/builtins/write_file.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/tools/session_update.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/acp/utils.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/autocompletion/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/autocompletion/base.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/autocompletion/path_completion.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/autocompletion/slash_command.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/cli.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/clipboard.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/commands.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/entrypoint.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/history_manager.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/plan_offer/adapters/http_whoami_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/plan_offer/decide_plan_offer.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/plan_offer/ports/whoami_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/terminal_setup.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/ansi_markdown.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/app.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/app.tcss +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/external_editor.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/handlers/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/handlers/event_handler.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/notifications/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/notifications/adapters/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/notifications/adapters/textual_notification_adapter.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/notifications/ports/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/notifications/ports/notification_port.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/approval_app.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/banner/banner.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/banner/petit_chat.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/braille_renderer.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/chat_input/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/chat_input/body.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/chat_input/completion_manager.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/chat_input/completion_popup.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/chat_input/container.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/chat_input/text_area.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/compact.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/config_app.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/context_progress.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/load_more.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/loading.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/messages.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/no_markup_static.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/path_display.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/proxy_setup_app.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/question_app.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/session_picker.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/spinner.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/status_message.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/teleport_message.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/tool_widgets.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/tools.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/widgets/vscode_compat.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/windowing/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/windowing/history.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/windowing/history_windowing.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/textual_ui/windowing/state.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/update_notifier/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/update_notifier/adapters/filesystem_update_cache_repository.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/update_notifier/adapters/github_update_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/update_notifier/adapters/pypi_update_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/update_notifier/ports/update_cache_repository.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/update_notifier/ports/update_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/update_notifier/update.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/cli/update_notifier/whats_new.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/agent_loop.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/agents/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/agents/manager.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/agents/models.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/auth/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/auth/crypto.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/auth/github.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/completers.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/file_indexer/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/file_indexer/ignore_rules.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/file_indexer/indexer.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/file_indexer/store.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/file_indexer/watcher.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/fuzzy.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/path_prompt.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/autocompletion/path_prompt_adapter.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/config/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/config/_settings.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/config/harness_files/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/config/harness_files/_harness_manager.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/config/harness_files/_paths.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/consultant.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/drydock_states.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/backend/anthropic.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/backend/base.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/backend/factory.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/backend/generic.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/backend/mistral.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/backend/reasoning_adapter.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/backend/vertex.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/exceptions.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/format.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/message_utils.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/llm/types.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/logger.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/middleware.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/output_formatters.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/paths/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/paths/_local_config_walk.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/paths/_vibe_home.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/paths/conventions.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/plan_session.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/programmatic.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/prompts/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/prompts/cli.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/prompts/compact.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/prompts/dangerous_directory.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/prompts/diagnostic.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/prompts/explore.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/prompts/planner.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/prompts/project_context.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/prompts/tests.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/proxy_setup.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/session/session_loader.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/session/session_logger.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/session/session_migration.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/session/state_file.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/skills/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/skills/manager.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/skills/models.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/skills/parser.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/slug.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/system_prompt.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/telemetry/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/telemetry/send.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/teleport/errors.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/teleport/git.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/teleport/nuage.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/teleport/teleport.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/teleport/types.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/base.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/ask_user_question.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/bash.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/exit_plan_mode.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/grep.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/ask_user_question.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/bash.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/grep.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/read_file.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/search_replace.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/swe_bench.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/task.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/todo.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/webfetch.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/websearch.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/prompts/write_file.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/read_file.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/search_replace.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/task.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/todo.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/webfetch.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/websearch.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/builtins/write_file.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/injection_guard.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/manager.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/mcp/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/mcp/registry.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/mcp/tools.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/mcp_sampling.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/ui.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/tools/utils.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/trusted_folders.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/types.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/core/utils.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/setup/onboarding/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/setup/onboarding/base.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/setup/onboarding/onboarding.tcss +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/setup/onboarding/screens/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/setup/onboarding/screens/api_key.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/setup/onboarding/screens/welcome.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/setup/trusted_folders/trust_folder_dialog.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/setup/trusted_folders/trust_folder_dialog.tcss +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/skills/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/skills/create-presentation/SKILL.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock/whats_new.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock-acp.spec +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/drydock_terms.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/flake.lock +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/flake.nix +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/scripts/README.md +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/scripts/bump_version.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/scripts/install.sh +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/scripts/prepare_release.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/conftest.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_acp.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_agent_thought.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_bash.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_compact_session_updates.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_content.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_initialize.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_list_sessions.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_load_session.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_multi_session.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_new_session.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_proxy_setup_acp.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_read_file.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_search_replace.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_set_config_option.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_set_mode.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_set_model.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_tool_call_session_update.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_utils.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/acp/test_write_file.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/autocompletion/test_file_indexer.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/autocompletion/test_fuzzy.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/autocompletion/test_path_completer_fuzzy.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/autocompletion/test_path_completer_recursive.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/autocompletion/test_path_completion_controller.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/autocompletion/test_path_prompt_transformer.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/autocompletion/test_slash_command_controller.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/autocompletion/test_ui_chat_autocompletion.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/backend/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/backend/data/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/backend/data/fireworks.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/backend/data/mistral.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/backend/test_anthropic_adapter.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/backend/test_backend.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/backend/test_reasoning_adapter.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/backend/test_vertex_anthropic_adapter.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/plan_offer/adapters/fake_whoami_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/plan_offer/test_decide_plan_offer.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/plan_offer/test_http_whoami_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_bell_notifications.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_braille_renderer.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_clipboard.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_commands.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_copy_shortcuts.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_external_editor.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_no_markup_static.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_question_app.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_spinner.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_switching_mode.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_ui_clipboard_notifications.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_ui_session_incremental_renderer.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_ui_session_resume.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/test_ui_skill_dispatch.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/textual_ui/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/cli/textual_ui/test_session_picker.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/conftest.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_agents.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_auth_crypto.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_auth_github.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_config_load_dotenv.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_config_paths.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_config_resolution.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_file_logging.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_plan_session.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_proxy_setup.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_slug.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_telemetry_send.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_teleport_git.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_teleport_nuage.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_teleport_service.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_trusted_folders.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/core/test_utils.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/e2e/common.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/e2e/conftest.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/e2e/mock_server.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/e2e/test_cli_tui_onboarding.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/e2e/test_cli_tui_streaming.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/e2e/test_cli_tui_tool_approval.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/mock/__init__.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/mock/mock_backend_factory.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/mock/mock_entrypoint.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/mock/utils.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/onboarding/test_run_onboarding.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/onboarding/test_ui_onboarding.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/session/test_session_loader.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/session/test_session_logger.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/session/test_session_migration.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/skills/conftest.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/skills/test_manager.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/skills/test_models.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/skills/test_parser.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_collapsed.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_expanded.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_basic_conversation/test_snapshot_shows_basic_conversation.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_code_block_horizontal_scrolling/test_snapshot_allows_horizontal_scrolling_for_long_code_blocks.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_empty_assistant_before_reasoning/test_snapshot_empty_assistant_removed_when_reasoning_starts.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_accept_edits_mode.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_auto_approve_mode.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_plan_mode.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_wraps_to_default.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_default_mode.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_pending.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_resolved.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_cancel_discards_changes.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_edit_existing_values.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_initial_empty.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_initial_with_values.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_error.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_new_values.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_answer_first_advance.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_first_answered_checkmark.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_initial.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_left_wraps.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_right.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_tab_to_second.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_initial.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_mixed_selection.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_navigate_to_submit.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_other_with_text.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_first.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_multiple.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_untoggle.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_initial.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_down.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_other.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_third_option.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_up_wraps.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_other_typing.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_buffered_reasoning_yields_before_content.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_interleaved_reasoning.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content_expanded.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_release_update_notification/test_snapshot_shows_release_update_notification.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_session_resume/test_snapshot_shows_resumed_session_messages.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_partial.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_updated.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_cancel_selected.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_multiple_commits.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_single_commit.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_complete.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_required.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_checking_git.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_complete.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_error.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_pushing.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_sending_token.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_starting_workflow.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_no_plan_message.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_switch_message.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_upgrade_message.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_whats_new_message.svg +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/base_snapshot_test_app.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/conftest.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/snap_compare.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_ask_user_question.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_basic_conversation.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_code_block_horizontal_scrolling.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_empty_assistant_before_reasoning.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_modes.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_parallel_tool_calls.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_proxy_setup.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_question_app.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_reasoning_content.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_release_update_notification.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_session_resume.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_streaming_tool_call.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_teleport.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/snapshots/test_ui_snapshot_whats_new.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/stubs/fake_backend.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/stubs/fake_client.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/stubs/fake_tool.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_agent_auto_compact.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_agent_backend.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_agent_observer_streaming.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_agent_stats.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_agent_tool_call.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_agents.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_cli_programmatic_preload.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_drydock_regression.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_drydock_tasks.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_history_manager.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_message_id.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_message_merging.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_middleware.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_reasoning_content.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_system_prompt.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_tagged_text.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_ui_external_editor.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/test_ui_input_history.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_ask_user_question.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_bash.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_exit_plan_mode.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_grep.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_invoke_context.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_manager_get_tool_config.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_mcp.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_mcp_sampling.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_task.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_ui_bash_execution.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_webfetch.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/tools/test_websearch.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/update_notifier/adapters/fake_update_cache_repository.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/update_notifier/adapters/fake_update_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/update_notifier/test_do_update.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/update_notifier/test_filesystem_update_cache_repository.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/update_notifier/test_github_update_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/update_notifier/test_pypi_update_gateway.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/update_notifier/test_ui_update_notification.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/update_notifier/test_update_use_case.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/tests/update_notifier/test_whats_new.py +0 -0
- {drydock_cli-0.6.4 → drydock_cli-0.6.6}/uv.lock +0 -0
|
@@ -37,7 +37,7 @@ AUTH_URL="https://${GITHUB_TOKEN}@github.com/fbobe321/drydock.git"
|
|
|
37
37
|
|
|
38
38
|
log "Running regression tests..."
|
|
39
39
|
cd "$DRYDOCK_SRC"
|
|
40
|
-
if ! python3 -m pytest tests/test_drydock_regression.py tests/test_drydock_tasks.py \
|
|
40
|
+
if ! python3 -m pytest tests/test_drydock_regression.py tests/test_drydock_tasks.py tests/test_loop_detection.py tests/test_agent_tasks.py \
|
|
41
41
|
-p no:xdist -p no:cov --override-ini="addopts=" -q 2>&1; then
|
|
42
42
|
log "TESTS FAILED — deploy aborted. Fix the tests before deploying."
|
|
43
43
|
exit 1
|
|
@@ -25,7 +25,7 @@ PYPI_TOKEN=$(cat "$TOKEN_FILE")
|
|
|
25
25
|
|
|
26
26
|
# --- Run tests ---
|
|
27
27
|
log "Running regression tests..."
|
|
28
|
-
if ! $PYTHON -m pytest tests/test_drydock_regression.py tests/test_drydock_tasks.py \
|
|
28
|
+
if ! $PYTHON -m pytest tests/test_drydock_regression.py tests/test_drydock_tasks.py tests/test_loop_detection.py tests/test_agent_tasks.py \
|
|
29
29
|
-p no:xdist -p no:cov --override-ini="addopts=" -q 2>&1; then
|
|
30
30
|
log "TESTS FAILED — publish aborted."
|
|
31
31
|
exit 1
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Agent task tests — spin up real agent loops with scripted LLM responses.
|
|
2
|
+
|
|
3
|
+
Each test simulates a real user task by providing scripted model responses
|
|
4
|
+
and verifying the agent produces the right events and behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from tests.conftest import build_test_agent_loop, build_test_vibe_config
|
|
15
|
+
from tests.mock.utils import mock_llm_chunk
|
|
16
|
+
from tests.stubs.fake_backend import FakeBackend
|
|
17
|
+
from drydock.core.agent_loop import AgentLoop
|
|
18
|
+
from drydock.core.agents.models import BuiltinAgentName
|
|
19
|
+
from drydock.core.tools.base import BaseToolConfig, ToolPermission
|
|
20
|
+
from drydock.core.types import (
|
|
21
|
+
AssistantEvent,
|
|
22
|
+
BaseEvent,
|
|
23
|
+
FunctionCall,
|
|
24
|
+
LLMMessage,
|
|
25
|
+
MessageList,
|
|
26
|
+
Role,
|
|
27
|
+
ToolCall,
|
|
28
|
+
ToolCallEvent,
|
|
29
|
+
ToolResultEvent,
|
|
30
|
+
UserMessageEvent,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def _run(agent: AgentLoop, prompt: str, max_events: int = 50) -> list[BaseEvent]:
|
|
35
|
+
events = []
|
|
36
|
+
async for ev in agent.act(prompt):
|
|
37
|
+
events.append(ev)
|
|
38
|
+
if len(events) >= max_events:
|
|
39
|
+
break
|
|
40
|
+
return events
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _count(events: list[BaseEvent], event_type: type) -> int:
|
|
44
|
+
return sum(1 for e in events if isinstance(e, event_type))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _agent(backend: FakeBackend) -> AgentLoop:
|
|
48
|
+
config = build_test_vibe_config(
|
|
49
|
+
system_prompt_id="tests",
|
|
50
|
+
include_project_context=False,
|
|
51
|
+
include_prompt_detail=False,
|
|
52
|
+
)
|
|
53
|
+
return build_test_agent_loop(
|
|
54
|
+
config=config,
|
|
55
|
+
agent_name=BuiltinAgentName.AUTO_APPROVE,
|
|
56
|
+
backend=backend,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ============================================================================
|
|
61
|
+
# Task 1: Simple greeting — no tools, no loops
|
|
62
|
+
# ============================================================================
|
|
63
|
+
|
|
64
|
+
@pytest.mark.asyncio
|
|
65
|
+
async def test_task_greeting_no_loop(telemetry_events):
|
|
66
|
+
"""Agent responds to greeting and stops without looping."""
|
|
67
|
+
backend = FakeBackend([
|
|
68
|
+
[mock_llm_chunk(content="Hello! How can I help?")],
|
|
69
|
+
])
|
|
70
|
+
events = await _run(_agent(backend), "hi")
|
|
71
|
+
|
|
72
|
+
assert _count(events, UserMessageEvent) == 1
|
|
73
|
+
text = [e for e in events if isinstance(e, AssistantEvent)]
|
|
74
|
+
assert len(text) >= 1
|
|
75
|
+
assert "Hello" in text[0].content
|
|
76
|
+
assert len(events) < 10 # Not looping
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ============================================================================
|
|
80
|
+
# Task 2: Agent stops after text-only response
|
|
81
|
+
# ============================================================================
|
|
82
|
+
|
|
83
|
+
@pytest.mark.asyncio
|
|
84
|
+
async def test_task_stops_on_text_response(telemetry_events):
|
|
85
|
+
"""Text-only response terminates the loop (no tool calls = done)."""
|
|
86
|
+
backend = FakeBackend([
|
|
87
|
+
[mock_llm_chunk(content="The answer is 42.")],
|
|
88
|
+
])
|
|
89
|
+
events = await _run(_agent(backend), "meaning of life?")
|
|
90
|
+
|
|
91
|
+
assert _count(events, AssistantEvent) >= 1
|
|
92
|
+
assert len(events) <= 5
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ============================================================================
|
|
96
|
+
# Task 3: Empty response doesn't crash
|
|
97
|
+
# ============================================================================
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_task_empty_response_no_crash(telemetry_events):
|
|
101
|
+
"""Empty LLM response handled gracefully."""
|
|
102
|
+
backend = FakeBackend([[mock_llm_chunk(content="")]])
|
|
103
|
+
events = await _run(_agent(backend), "hello")
|
|
104
|
+
assert _count(events, UserMessageEvent) == 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ============================================================================
|
|
108
|
+
# Task 4: Message ordering maintained across turns
|
|
109
|
+
# ============================================================================
|
|
110
|
+
|
|
111
|
+
@pytest.mark.asyncio
|
|
112
|
+
async def test_task_message_ordering_clean(telemetry_events):
|
|
113
|
+
"""No user-after-tool violations in message history."""
|
|
114
|
+
backend = FakeBackend([
|
|
115
|
+
[mock_llm_chunk(content="First response.")],
|
|
116
|
+
])
|
|
117
|
+
agent = _agent(backend)
|
|
118
|
+
await _run(agent, "test message ordering")
|
|
119
|
+
|
|
120
|
+
for i in range(1, len(agent.messages)):
|
|
121
|
+
assert not (
|
|
122
|
+
agent.messages[i].role == Role.user
|
|
123
|
+
and agent.messages[i - 1].role == Role.tool
|
|
124
|
+
), f"user after tool at position {i}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ============================================================================
|
|
128
|
+
# Task 5: Messages remain a MessageList (not plain list)
|
|
129
|
+
# ============================================================================
|
|
130
|
+
|
|
131
|
+
@pytest.mark.asyncio
|
|
132
|
+
async def test_task_messagelist_integrity(telemetry_events):
|
|
133
|
+
"""Agent messages stay as MessageList, never replaced with plain list."""
|
|
134
|
+
backend = FakeBackend([
|
|
135
|
+
[mock_llm_chunk(content="Working on it.")],
|
|
136
|
+
])
|
|
137
|
+
agent = _agent(backend)
|
|
138
|
+
await _run(agent, "test integrity")
|
|
139
|
+
|
|
140
|
+
assert isinstance(agent.messages, MessageList)
|
|
141
|
+
assert hasattr(agent.messages, "reset")
|
|
142
|
+
assert hasattr(agent.messages, "append")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ============================================================================
|
|
146
|
+
# Task 6: Circuit breaker state initialized
|
|
147
|
+
# ============================================================================
|
|
148
|
+
|
|
149
|
+
@pytest.mark.asyncio
|
|
150
|
+
async def test_task_circuit_breaker_initialized(telemetry_events):
|
|
151
|
+
"""Agent has circuit breaker history dict after creation."""
|
|
152
|
+
backend = FakeBackend([[mock_llm_chunk(content="ok")]])
|
|
153
|
+
agent = _agent(backend)
|
|
154
|
+
assert hasattr(agent, "_tool_call_history")
|
|
155
|
+
assert isinstance(agent._tool_call_history, dict)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ============================================================================
|
|
159
|
+
# Task 7: Middleware turn limit stops agent
|
|
160
|
+
# ============================================================================
|
|
161
|
+
|
|
162
|
+
@pytest.mark.asyncio
|
|
163
|
+
async def test_task_turn_limit_stops_agent(telemetry_events):
|
|
164
|
+
"""max_turns=1 limits agent to a single turn."""
|
|
165
|
+
backend = FakeBackend([
|
|
166
|
+
[mock_llm_chunk(content="First turn.")],
|
|
167
|
+
[mock_llm_chunk(content="Should not reach this.")],
|
|
168
|
+
])
|
|
169
|
+
config = build_test_vibe_config(
|
|
170
|
+
system_prompt_id="tests",
|
|
171
|
+
include_project_context=False,
|
|
172
|
+
include_prompt_detail=False,
|
|
173
|
+
)
|
|
174
|
+
agent = build_test_agent_loop(
|
|
175
|
+
config=config,
|
|
176
|
+
agent_name=BuiltinAgentName.AUTO_APPROVE,
|
|
177
|
+
backend=backend,
|
|
178
|
+
max_turns=1,
|
|
179
|
+
)
|
|
180
|
+
events = await _run(agent, "do many things")
|
|
181
|
+
|
|
182
|
+
# With max_turns=1, agent should stop after 1 turn
|
|
183
|
+
# Either via middleware stop or by natural text-only response
|
|
184
|
+
text = [e for e in events if isinstance(e, AssistantEvent)]
|
|
185
|
+
assert len(text) >= 1
|
|
186
|
+
assert len(events) <= 10
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ============================================================================
|
|
190
|
+
# Task 8: Multi-response conversation
|
|
191
|
+
# ============================================================================
|
|
192
|
+
|
|
193
|
+
@pytest.mark.asyncio
|
|
194
|
+
async def test_task_multi_turn_conversation(telemetry_events):
|
|
195
|
+
"""Agent handles multiple text-only turns (when nudged to continue)."""
|
|
196
|
+
backend = FakeBackend([
|
|
197
|
+
[mock_llm_chunk(content="Let me think about this...")],
|
|
198
|
+
])
|
|
199
|
+
agent = _agent(backend)
|
|
200
|
+
events = await _run(agent, "explain the codebase")
|
|
201
|
+
|
|
202
|
+
# Should have at least user message + 1 assistant response
|
|
203
|
+
assert _count(events, UserMessageEvent) == 1
|
|
204
|
+
assert _count(events, AssistantEvent) >= 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ============================================================================
|
|
208
|
+
# Task 9: Backend exception doesn't crash permanently
|
|
209
|
+
# ============================================================================
|
|
210
|
+
|
|
211
|
+
@pytest.mark.asyncio
|
|
212
|
+
async def test_task_backend_error_recovery(telemetry_events):
|
|
213
|
+
"""Agent survives an LLM backend error."""
|
|
214
|
+
backend = FakeBackend(
|
|
215
|
+
exception_to_raise=RuntimeError("API error from vllm: connection refused")
|
|
216
|
+
)
|
|
217
|
+
agent = _agent(backend)
|
|
218
|
+
|
|
219
|
+
# Should not raise — agent catches and handles
|
|
220
|
+
events = await _run(agent, "test error handling")
|
|
221
|
+
# Will have limited events since backend keeps failing
|
|
222
|
+
assert _count(events, UserMessageEvent) == 1
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ============================================================================
|
|
226
|
+
# Task 10: System prompt present in messages
|
|
227
|
+
# ============================================================================
|
|
228
|
+
|
|
229
|
+
@pytest.mark.asyncio
|
|
230
|
+
async def test_task_system_prompt_present(telemetry_events):
|
|
231
|
+
"""Agent has a system prompt as first message."""
|
|
232
|
+
backend = FakeBackend([[mock_llm_chunk(content="ok")]])
|
|
233
|
+
agent = _agent(backend)
|
|
234
|
+
await _run(agent, "test")
|
|
235
|
+
|
|
236
|
+
assert len(agent.messages) >= 2
|
|
237
|
+
assert agent.messages[0].role == Role.system
|
|
238
|
+
assert len(agent.messages[0].content) > 50 # Non-trivial system prompt
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Comprehensive loop detection tests.
|
|
2
|
+
|
|
3
|
+
Simulates real message histories that trigger each loop detection mechanism.
|
|
4
|
+
These test the exact scenarios users report:
|
|
5
|
+
- Running `ls -ltr` 5+ times (circuit breaker)
|
|
6
|
+
- Exact same grep repeated 8+ times (Check 1: REPEAT_WARNING)
|
|
7
|
+
- 10 consecutive bash calls with different args (Check 2: same-tool)
|
|
8
|
+
- Reading the same file 5+ times with different offsets (Check 3: same-file)
|
|
9
|
+
- Alternating grep→read_file→grep→read_file (Check 4: alternating)
|
|
10
|
+
- Bash abuse: 10+ bash calls without making an edit
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from types import SimpleNamespace
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from drydock.core.agent_loop import (
|
|
21
|
+
AgentLoop,
|
|
22
|
+
MAX_TOOL_TURNS,
|
|
23
|
+
REPEAT_FORCE_STOP_THRESHOLD,
|
|
24
|
+
REPEAT_WARNING_THRESHOLD,
|
|
25
|
+
)
|
|
26
|
+
from drydock.core.types import LLMMessage, MessageList, Role, ToolCall, FunctionCall
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _make_agent() -> AgentLoop:
|
|
30
|
+
"""Create a minimal AgentLoop for testing loop detection."""
|
|
31
|
+
al = object.__new__(AgentLoop)
|
|
32
|
+
al.messages = MessageList()
|
|
33
|
+
al._tool_call_history = {}
|
|
34
|
+
return al
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _add_tool_call(al: AgentLoop, tool_name: str, arguments: str = "{}") -> None:
|
|
38
|
+
"""Add an assistant message with a tool call + a tool result."""
|
|
39
|
+
al.messages.append(LLMMessage(
|
|
40
|
+
role=Role.assistant,
|
|
41
|
+
content="",
|
|
42
|
+
tool_calls=[ToolCall(
|
|
43
|
+
id=f"call_{len(al.messages)}",
|
|
44
|
+
function=FunctionCall(name=tool_name, arguments=arguments),
|
|
45
|
+
)],
|
|
46
|
+
))
|
|
47
|
+
al.messages.append(LLMMessage(
|
|
48
|
+
role=Role.tool,
|
|
49
|
+
content=f"Result of {tool_name}",
|
|
50
|
+
))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# Circuit Breaker Tests
|
|
55
|
+
# ============================================================================
|
|
56
|
+
|
|
57
|
+
class TestCircuitBreaker:
|
|
58
|
+
"""The circuit breaker blocks the EXACT same tool call after 2 attempts."""
|
|
59
|
+
|
|
60
|
+
def test_first_call_allowed(self):
|
|
61
|
+
al = _make_agent()
|
|
62
|
+
tc = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls -ltr"}')
|
|
63
|
+
assert al._circuit_breaker_check(tc) is None
|
|
64
|
+
|
|
65
|
+
def test_second_call_allowed(self):
|
|
66
|
+
al = _make_agent()
|
|
67
|
+
tc = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls -ltr"}')
|
|
68
|
+
al._circuit_breaker_record(tc, "file1.py\nfile2.py")
|
|
69
|
+
assert al._circuit_breaker_check(tc) is None
|
|
70
|
+
|
|
71
|
+
def test_third_call_blocked(self):
|
|
72
|
+
al = _make_agent()
|
|
73
|
+
tc = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls -ltr"}')
|
|
74
|
+
al._circuit_breaker_record(tc, "file1.py\nfile2.py")
|
|
75
|
+
al._circuit_breaker_record(tc, "file1.py\nfile2.py")
|
|
76
|
+
result = al._circuit_breaker_check(tc)
|
|
77
|
+
assert result is not None
|
|
78
|
+
assert "CIRCUIT BREAKER" in result
|
|
79
|
+
|
|
80
|
+
def test_blocked_message_contains_previous_result(self):
|
|
81
|
+
al = _make_agent()
|
|
82
|
+
tc = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls -ltr"}')
|
|
83
|
+
al._circuit_breaker_record(tc, "important_file.py")
|
|
84
|
+
al._circuit_breaker_record(tc, "important_file.py")
|
|
85
|
+
result = al._circuit_breaker_check(tc)
|
|
86
|
+
assert "important_file.py" in result
|
|
87
|
+
|
|
88
|
+
def test_blocked_message_contains_alternatives(self):
|
|
89
|
+
al = _make_agent()
|
|
90
|
+
tc = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls -ltr"}')
|
|
91
|
+
al._circuit_breaker_record(tc, "output")
|
|
92
|
+
al._circuit_breaker_record(tc, "output")
|
|
93
|
+
result = al._circuit_breaker_check(tc)
|
|
94
|
+
assert "DIFFERENT" in result or "different" in result
|
|
95
|
+
|
|
96
|
+
def test_different_args_not_blocked(self):
|
|
97
|
+
al = _make_agent()
|
|
98
|
+
tc1 = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls -ltr"}')
|
|
99
|
+
tc2 = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls -la"}')
|
|
100
|
+
al._circuit_breaker_record(tc1, "r1")
|
|
101
|
+
al._circuit_breaker_record(tc1, "r1")
|
|
102
|
+
assert al._circuit_breaker_check(tc1) is not None # Blocked
|
|
103
|
+
assert al._circuit_breaker_check(tc2) is None # Not blocked
|
|
104
|
+
|
|
105
|
+
def test_different_tools_not_blocked(self):
|
|
106
|
+
al = _make_agent()
|
|
107
|
+
tc1 = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls"}')
|
|
108
|
+
tc2 = SimpleNamespace(tool_name="grep", raw_arguments='{"pattern":"foo"}')
|
|
109
|
+
al._circuit_breaker_record(tc1, "r1")
|
|
110
|
+
al._circuit_breaker_record(tc1, "r1")
|
|
111
|
+
assert al._circuit_breaker_check(tc1) is not None
|
|
112
|
+
assert al._circuit_breaker_check(tc2) is None
|
|
113
|
+
|
|
114
|
+
def test_already_attempted_summary(self):
|
|
115
|
+
al = _make_agent()
|
|
116
|
+
tc = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls"}')
|
|
117
|
+
al._circuit_breaker_record(tc, "output1")
|
|
118
|
+
al._circuit_breaker_record(tc, "output1")
|
|
119
|
+
result = al._circuit_breaker_check(tc)
|
|
120
|
+
assert "ALREADY ATTEMPTED" in result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ============================================================================
|
|
124
|
+
# Check 1: Exact Same Tool Call Repeated (REPEAT_WARNING_THRESHOLD)
|
|
125
|
+
# ============================================================================
|
|
126
|
+
|
|
127
|
+
class TestExactRepeatDetection:
|
|
128
|
+
"""Detects the same tool call (name + args) repeated N+ times."""
|
|
129
|
+
|
|
130
|
+
def test_no_repeats_returns_none(self):
|
|
131
|
+
al = _make_agent()
|
|
132
|
+
_add_tool_call(al, "bash", '{"command":"ls"}')
|
|
133
|
+
_add_tool_call(al, "grep", '{"pattern":"foo"}')
|
|
134
|
+
_add_tool_call(al, "read_file", '{"path":"x.py"}')
|
|
135
|
+
assert al._check_tool_call_repetition() is None
|
|
136
|
+
|
|
137
|
+
def test_warning_at_threshold(self):
|
|
138
|
+
al = _make_agent()
|
|
139
|
+
args = '{"command":"ls -ltr"}'
|
|
140
|
+
for _ in range(REPEAT_WARNING_THRESHOLD):
|
|
141
|
+
_add_tool_call(al, "bash", args)
|
|
142
|
+
result = al._check_tool_call_repetition()
|
|
143
|
+
assert result is not None
|
|
144
|
+
assert "WARNING" in result
|
|
145
|
+
|
|
146
|
+
def test_below_threshold_returns_none(self):
|
|
147
|
+
al = _make_agent()
|
|
148
|
+
args = '{"command":"ls -ltr"}'
|
|
149
|
+
for _ in range(REPEAT_WARNING_THRESHOLD - 1):
|
|
150
|
+
_add_tool_call(al, "bash", args)
|
|
151
|
+
assert al._check_tool_call_repetition() is None
|
|
152
|
+
|
|
153
|
+
def test_force_stop_at_threshold(self):
|
|
154
|
+
"""25 exact repeats. Check 2 (same-tool at 8) catches it first as WARNING."""
|
|
155
|
+
al = _make_agent()
|
|
156
|
+
args = '{"command":"ls -ltr"}'
|
|
157
|
+
for _ in range(REPEAT_FORCE_STOP_THRESHOLD):
|
|
158
|
+
_add_tool_call(al, "bash", args)
|
|
159
|
+
result = al._check_tool_call_repetition()
|
|
160
|
+
# Same-tool consecutive check (8 for bash) fires before exact-repeat (25)
|
|
161
|
+
assert result is not None
|
|
162
|
+
assert "WARNING" in result or result == "FORCE_STOP"
|
|
163
|
+
|
|
164
|
+
def test_mixed_calls_dont_trigger(self):
|
|
165
|
+
al = _make_agent()
|
|
166
|
+
for i in range(20):
|
|
167
|
+
_add_tool_call(al, "bash", json.dumps({"command": f"cmd_{i}"}))
|
|
168
|
+
# All different args — shouldn't trigger exact repeat
|
|
169
|
+
result = al._check_tool_call_repetition()
|
|
170
|
+
# Might trigger same-tool check but not exact repeat
|
|
171
|
+
if result:
|
|
172
|
+
assert "FORCE_STOP" not in result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ============================================================================
|
|
176
|
+
# Check 2: Same Tool Consecutive (Different Args)
|
|
177
|
+
# ============================================================================
|
|
178
|
+
|
|
179
|
+
class TestSameToolConsecutive:
|
|
180
|
+
"""Detects the same tool used N+ times in a row with different args."""
|
|
181
|
+
|
|
182
|
+
def test_8_consecutive_bash_triggers_warning(self):
|
|
183
|
+
al = _make_agent()
|
|
184
|
+
for i in range(8):
|
|
185
|
+
_add_tool_call(al, "bash", json.dumps({"command": f"cmd_{i}"}))
|
|
186
|
+
result = al._check_tool_call_repetition()
|
|
187
|
+
assert result is not None
|
|
188
|
+
assert "WARNING" in result
|
|
189
|
+
assert "bash" in result
|
|
190
|
+
|
|
191
|
+
def test_7_consecutive_bash_no_warning(self):
|
|
192
|
+
al = _make_agent()
|
|
193
|
+
for i in range(7):
|
|
194
|
+
_add_tool_call(al, "bash", json.dumps({"command": f"cmd_{i}"}))
|
|
195
|
+
result = al._check_tool_call_repetition()
|
|
196
|
+
assert result is None
|
|
197
|
+
|
|
198
|
+
def test_10_consecutive_grep_triggers_warning(self):
|
|
199
|
+
al = _make_agent()
|
|
200
|
+
for i in range(10):
|
|
201
|
+
_add_tool_call(al, "grep", json.dumps({"pattern": f"pattern_{i}"}))
|
|
202
|
+
result = al._check_tool_call_repetition()
|
|
203
|
+
assert result is not None
|
|
204
|
+
assert "WARNING" in result
|
|
205
|
+
|
|
206
|
+
def test_9_consecutive_grep_no_warning(self):
|
|
207
|
+
al = _make_agent()
|
|
208
|
+
for i in range(9):
|
|
209
|
+
_add_tool_call(al, "grep", json.dumps({"pattern": f"pattern_{i}"}))
|
|
210
|
+
assert al._check_tool_call_repetition() is None
|
|
211
|
+
|
|
212
|
+
def test_interleaved_tools_dont_trigger(self):
|
|
213
|
+
al = _make_agent()
|
|
214
|
+
for i in range(20):
|
|
215
|
+
tool = "bash" if i % 2 == 0 else "grep"
|
|
216
|
+
_add_tool_call(al, tool, json.dumps({"arg": f"val_{i}"}))
|
|
217
|
+
# Not consecutive same tool — shouldn't trigger Check 2
|
|
218
|
+
# (might trigger Check 4 alternating though)
|
|
219
|
+
result = al._check_tool_call_repetition()
|
|
220
|
+
# Just verify it doesn't crash
|
|
221
|
+
assert result is None or "WARNING" in result
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ============================================================================
|
|
225
|
+
# Check 3: Same File Read 5+ Times
|
|
226
|
+
# ============================================================================
|
|
227
|
+
|
|
228
|
+
class TestSameFileReadLoop:
|
|
229
|
+
"""Detects reading the same file 5+ times (different offsets)."""
|
|
230
|
+
|
|
231
|
+
def test_5_reads_same_file_triggers(self):
|
|
232
|
+
al = _make_agent()
|
|
233
|
+
for i in range(5):
|
|
234
|
+
_add_tool_call(al, "read_file", json.dumps({
|
|
235
|
+
"path": "src/auth.py", "offset": i * 50, "limit": 50
|
|
236
|
+
}))
|
|
237
|
+
result = al._check_tool_call_repetition()
|
|
238
|
+
assert result is not None
|
|
239
|
+
assert "WARNING" in result
|
|
240
|
+
|
|
241
|
+
def test_4_reads_same_file_no_trigger(self):
|
|
242
|
+
al = _make_agent()
|
|
243
|
+
for i in range(4):
|
|
244
|
+
_add_tool_call(al, "read_file", json.dumps({
|
|
245
|
+
"path": "src/auth.py", "offset": i * 50
|
|
246
|
+
}))
|
|
247
|
+
# 4 reads shouldn't trigger (threshold is 5)
|
|
248
|
+
# But might trigger same-tool-consecutive if we hit that threshold
|
|
249
|
+
result = al._check_tool_call_repetition()
|
|
250
|
+
# At 4 reads, same-tool limit for read_file is 10, so no trigger
|
|
251
|
+
assert result is None
|
|
252
|
+
|
|
253
|
+
def test_5_reads_different_files_no_trigger(self):
|
|
254
|
+
al = _make_agent()
|
|
255
|
+
for i in range(5):
|
|
256
|
+
_add_tool_call(al, "read_file", json.dumps({
|
|
257
|
+
"path": f"src/file_{i}.py"
|
|
258
|
+
}))
|
|
259
|
+
result = al._check_tool_call_repetition()
|
|
260
|
+
assert result is None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ============================================================================
|
|
264
|
+
# Check 4: Alternating Tool Pattern (A→B→A→B)
|
|
265
|
+
# ============================================================================
|
|
266
|
+
|
|
267
|
+
class TestAlternatingPattern:
|
|
268
|
+
"""Detects A→B→A→B→A→B→A→B alternating patterns."""
|
|
269
|
+
|
|
270
|
+
def test_8_alternating_triggers(self):
|
|
271
|
+
al = _make_agent()
|
|
272
|
+
for i in range(8):
|
|
273
|
+
tool = "grep" if i % 2 == 0 else "read_file"
|
|
274
|
+
_add_tool_call(al, tool, json.dumps({"arg": f"val_{i}"}))
|
|
275
|
+
result = al._check_tool_call_repetition()
|
|
276
|
+
assert result is not None
|
|
277
|
+
assert "WARNING" in result
|
|
278
|
+
|
|
279
|
+
def test_6_alternating_no_trigger(self):
|
|
280
|
+
al = _make_agent()
|
|
281
|
+
for i in range(6):
|
|
282
|
+
tool = "grep" if i % 2 == 0 else "read_file"
|
|
283
|
+
_add_tool_call(al, tool, json.dumps({"arg": f"val_{i}"}))
|
|
284
|
+
result = al._check_tool_call_repetition()
|
|
285
|
+
assert result is None
|
|
286
|
+
|
|
287
|
+
def test_3_tool_cycle_no_trigger(self):
|
|
288
|
+
"""A→B→C→A→B→C shouldn't trigger the 2-tool alternating check."""
|
|
289
|
+
al = _make_agent()
|
|
290
|
+
tools = ["grep", "read_file", "bash"]
|
|
291
|
+
for i in range(9):
|
|
292
|
+
_add_tool_call(al, tools[i % 3], json.dumps({"arg": f"val_{i}"}))
|
|
293
|
+
result = al._check_tool_call_repetition()
|
|
294
|
+
# 3-tool cycles are not detected by Check 4 (which only checks 2-tool)
|
|
295
|
+
# Might trigger same-tool-consecutive for individual tools though
|
|
296
|
+
assert result is None or "FORCE_STOP" not in result
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ============================================================================
|
|
300
|
+
# Threshold Constants
|
|
301
|
+
# ============================================================================
|
|
302
|
+
|
|
303
|
+
class TestThresholdConstants:
|
|
304
|
+
"""Verify threshold constants are reasonable."""
|
|
305
|
+
|
|
306
|
+
def test_max_tool_turns_ceiling(self):
|
|
307
|
+
assert MAX_TOOL_TURNS <= 200
|
|
308
|
+
assert MAX_TOOL_TURNS >= 50
|
|
309
|
+
|
|
310
|
+
def test_warning_before_force_stop(self):
|
|
311
|
+
assert REPEAT_WARNING_THRESHOLD < REPEAT_FORCE_STOP_THRESHOLD
|
|
312
|
+
|
|
313
|
+
def test_warning_threshold_not_too_low(self):
|
|
314
|
+
assert REPEAT_WARNING_THRESHOLD >= 6
|
|
315
|
+
|
|
316
|
+
def test_force_stop_not_too_high(self):
|
|
317
|
+
assert REPEAT_FORCE_STOP_THRESHOLD <= 30
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ============================================================================
|
|
321
|
+
# Real-World Loop Scenarios
|
|
322
|
+
# ============================================================================
|
|
323
|
+
|
|
324
|
+
class TestRealWorldLoops:
|
|
325
|
+
"""Simulate actual user-reported looping scenarios."""
|
|
326
|
+
|
|
327
|
+
def test_ls_ltr_loop(self):
|
|
328
|
+
"""User reports: `ls -ltr` runs 5+ times."""
|
|
329
|
+
al = _make_agent()
|
|
330
|
+
tc = SimpleNamespace(tool_name="bash", raw_arguments='{"command":"ls -ltr"}')
|
|
331
|
+
|
|
332
|
+
# First 2 calls go through
|
|
333
|
+
al._circuit_breaker_record(tc, "total 48\n-rw-r--r-- 1 user user 1234 auth.py")
|
|
334
|
+
al._circuit_breaker_record(tc, "total 48\n-rw-r--r-- 1 user user 1234 auth.py")
|
|
335
|
+
|
|
336
|
+
# 3rd call is blocked
|
|
337
|
+
assert al._circuit_breaker_check(tc) is not None
|
|
338
|
+
|
|
339
|
+
def test_grep_same_pattern_loop(self):
|
|
340
|
+
"""Model greps for the same pattern 10 times."""
|
|
341
|
+
al = _make_agent()
|
|
342
|
+
for _ in range(REPEAT_WARNING_THRESHOLD):
|
|
343
|
+
_add_tool_call(al, "grep", '{"pattern":"def authenticate","path":"."}')
|
|
344
|
+
assert al._check_tool_call_repetition() is not None
|
|
345
|
+
|
|
346
|
+
def test_read_file_offset_evasion(self):
|
|
347
|
+
"""Model reads same file with incrementing offset to evade detection."""
|
|
348
|
+
al = _make_agent()
|
|
349
|
+
for i in range(5):
|
|
350
|
+
_add_tool_call(al, "read_file", json.dumps({
|
|
351
|
+
"path": "django/db/models/query.py",
|
|
352
|
+
"offset": i * 100,
|
|
353
|
+
"limit": 100,
|
|
354
|
+
}))
|
|
355
|
+
assert al._check_tool_call_repetition() is not None
|
|
356
|
+
|
|
357
|
+
def test_bash_exploration_loop(self):
|
|
358
|
+
"""Model runs 8+ different bash commands exploring the filesystem."""
|
|
359
|
+
al = _make_agent()
|
|
360
|
+
commands = [
|
|
361
|
+
"find . -name '*.py'", "ls src/", "cat README.md",
|
|
362
|
+
"wc -l src/*.py", "head -20 setup.py", "ls -la tests/",
|
|
363
|
+
"find . -type f | wc -l", "du -sh src/",
|
|
364
|
+
]
|
|
365
|
+
for cmd in commands:
|
|
366
|
+
_add_tool_call(al, "bash", json.dumps({"command": cmd}))
|
|
367
|
+
result = al._check_tool_call_repetition()
|
|
368
|
+
assert result is not None # 8 consecutive bash calls
|
|
369
|
+
|
|
370
|
+
def test_grep_read_alternation(self):
|
|
371
|
+
"""Model alternates between grep and read_file without making progress."""
|
|
372
|
+
al = _make_agent()
|
|
373
|
+
for i in range(8):
|
|
374
|
+
if i % 2 == 0:
|
|
375
|
+
_add_tool_call(al, "grep", json.dumps({"pattern": f"func_{i//2}"}))
|
|
376
|
+
else:
|
|
377
|
+
_add_tool_call(al, "read_file", json.dumps({"path": f"file_{i//2}.py"}))
|
|
378
|
+
assert al._check_tool_call_repetition() is not None
|
|
379
|
+
|
|
380
|
+
def test_normal_workflow_no_false_positive(self):
|
|
381
|
+
"""A normal workflow: grep → read → edit → verify should NOT trigger."""
|
|
382
|
+
al = _make_agent()
|
|
383
|
+
_add_tool_call(al, "grep", '{"pattern":"def login"}')
|
|
384
|
+
_add_tool_call(al, "read_file", '{"path":"auth.py","offset":10,"limit":20}')
|
|
385
|
+
_add_tool_call(al, "search_replace", '{"file_path":"auth.py","content":"..."}')
|
|
386
|
+
_add_tool_call(al, "read_file", '{"path":"auth.py","offset":10,"limit":20}')
|
|
387
|
+
assert al._check_tool_call_repetition() is None
|
|
388
|
+
|
|
389
|
+
def test_investigate_then_fix_no_false_positive(self):
|
|
390
|
+
"""3 greps + 2 reads + 1 edit = normal bug fix workflow."""
|
|
391
|
+
al = _make_agent()
|
|
392
|
+
_add_tool_call(al, "grep", '{"pattern":"class User"}')
|
|
393
|
+
_add_tool_call(al, "grep", '{"pattern":"def save"}')
|
|
394
|
+
_add_tool_call(al, "read_file", '{"path":"models.py"}')
|
|
395
|
+
_add_tool_call(al, "grep", '{"pattern":"validate"}')
|
|
396
|
+
_add_tool_call(al, "read_file", '{"path":"validators.py"}')
|
|
397
|
+
_add_tool_call(al, "search_replace", '{"file_path":"models.py","content":"..."}')
|
|
398
|
+
assert al._check_tool_call_repetition() is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|