mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e29a4147f2 | |||
| aa15c9d385 | |||
| 8ad89e368a | |||
| 29ba94fc0f | |||
| 8956f92f6a | |||
| 753e3c4205 | |||
| ecd573febc | |||
| 325a558fbc | |||
| 666c186826 | |||
| 2d2dbf1561 | |||
| abac25cc4c | |||
| 70b21d16bd | |||
| bf82f75ae4 | |||
| a8bce3724f | |||
| e109f7e58e | |||
| a20f299579 | |||
| bf77da7849 | |||
| 869ea59ecd | |||
| f093c14ad3 | |||
| 9d3a0a02b8 | |||
| 35bab5070d | |||
| d03b9775b5 | |||
| fab4532f6b | |||
| d33913e036 | |||
| e52aee168e | |||
| c27b191358 | |||
| 22c5ad85d9 | |||
| 0180ce77b1 | |||
| 2f14e53746 | |||
| 52723061b1 | |||
| 42f1fc92fa | |||
| 3f8bc8a7ea | |||
| f869ad995c | |||
| 74c942c911 | |||
| eed7e2dd6e | |||
| 663e36109c | |||
| e92e4a1cbc | |||
| 61ce673400 | |||
| b95840db0c | |||
| 003ebc0ded | |||
| df8bbc2b67 | |||
| eb5be2ab63 | |||
| 81f2b08a89 | |||
| cb72a06ca3 | |||
| 340c2310d1 | |||
| f4e5fb2873 | |||
| 1a7003a705 | |||
| 8d097efb4f | |||
| 2e98fc8fb3 | |||
| e3e00ed70a | |||
| 96d1992823 | |||
| 7a3a0d8c0c | |||
| fdffca18e0 | |||
| b10416e0a3 | |||
| 1f462d2417 | |||
| 0a6ff463db | |||
| 9ff15bf94f | |||
| 6c48013601 | |||
| 07fcb786af | |||
| ce42e22105 | |||
| 14ee6d7afe | |||
| 57391d6e66 | |||
| a7bb73ded2 | |||
| f4b123f73b | |||
| ae31a24c29 | |||
| 3a478c2303 | |||
| 82b5325792 | |||
| 265e8ae5f4 | |||
| 0cbf50576d | |||
| 745038b394 | |||
| b018567d53 | |||
| 30e39e85d0 | |||
| 4443417c75 | |||
| efbff2e655 | |||
| 63565982aa | |||
| b07fddcb71 | |||
| 99b50d038e | |||
| 1ddfa99c57 | |||
| 0c03e257b7 | |||
| 85e3a00d9d | |||
| edd51102ad | |||
| f5fccab1f6 | |||
| ef12bc5381 | |||
| b197e0af47 | |||
| 341b695ad3 | |||
| d46d99a35e | |||
| 653168fc3d | |||
| cb5e7f0130 | |||
| 312b9fbfb1 | |||
| ba599c7dd6 | |||
| 7e359eda4a | |||
| f7c806c119 | |||
| ff25e794ef | |||
| a371562d94 | |||
| 425ccc9b1f | |||
| 1afe7f1058 | |||
| 3188646195 | |||
| 6772227c9d | |||
| 6a6dc93e03 | |||
| 1a715d2ec4 | |||
| 4615548477 | |||
| b12b426e3d | |||
| a1107a2c30 | |||
| af0becd65b | |||
| 13839b4273 | |||
| 7860055f8c | |||
| 2b40a92943 | |||
| 6c88b10c59 | |||
| 8688634950 | |||
| 6e35ac49c1 | |||
| 9bdc8dda6c | |||
| 75f3f282af | |||
| 4a5891cbea | |||
| 707cb07f4f | |||
| 61c709b7c7 | |||
| 1c72676483 | |||
| 52ac2729f7 | |||
| 5fa2634d60 | |||
| 478b225d11 | |||
| ce82545437 | |||
| 93d2e4a338 | |||
| ff48f8beba | |||
| e930cd0aef | |||
| 6655ec0731 | |||
| 669e284dc5 | |||
| 8140d2e05a | |||
| f7424970f0 |
@@ -24,6 +24,10 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install tmux
|
||||
run: sudo apt-get update && sudo apt-get install -y tmux
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
uses: docker/setup-qemu-action@v3.4.0
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
- name: Login to GHCR
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
uses: docker/setup-qemu-action@v3.4.0
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
- name: Login to GHCR
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: make install-python-dependencies
|
||||
- name: Run runtime tests
|
||||
- name: Run docker runtime tests
|
||||
run: |
|
||||
# We install pytest-xdist in order to run tests across CPUs
|
||||
poetry run pip install pytest-xdist
|
||||
|
||||
@@ -88,12 +88,10 @@ jobs:
|
||||
run: |
|
||||
python -m pip index versions openhands-ai > openhands_versions.txt
|
||||
OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()')
|
||||
# Ensure requirements.txt ends with newline before appending
|
||||
if [ -f requirements.txt ] && [ -s requirements.txt ]; then
|
||||
sed -i -e '$a\' requirements.txt
|
||||
fi
|
||||
echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt
|
||||
cat requirements.txt
|
||||
|
||||
# Create a new requirements.txt locally within the workflow, ensuring no reference to the repo's file
|
||||
echo "openhands-ai==${OPENHANDS_VERSION}" > /tmp/requirements.txt
|
||||
cat /tmp/requirements.txt
|
||||
|
||||
- name: Cache pip dependencies
|
||||
if: |
|
||||
@@ -111,9 +109,9 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
|
||||
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
|
||||
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
|
||||
${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }}
|
||||
|
||||
- name: Check required environment variables
|
||||
env:
|
||||
@@ -177,7 +175,7 @@ jobs:
|
||||
echo "SANDBOX_ENV_BASE_CONTAINER_IMAGE=${{ inputs.base_container_image }}" >> $GITHUB_ENV
|
||||
|
||||
# Set branch variables
|
||||
echo "TARGET_BRANCH=${{ inputs.target_branch }}" >> $GITHUB_ENV
|
||||
echo "TARGET_BRANCH=${{ inputs.target_branch || 'main' }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Comment on issue with start message
|
||||
uses: actions/github-script@v7
|
||||
@@ -225,13 +223,14 @@ jobs:
|
||||
} else {
|
||||
console.log("Installing from requirements.txt...");
|
||||
await exec.exec("python -m pip install --upgrade pip");
|
||||
await exec.exec("pip install -r requirements.txt");
|
||||
await exec.exec("pip install -r /tmp/requirements.txt");
|
||||
}
|
||||
|
||||
- name: Attempt to resolve issue
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }}
|
||||
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
|
||||
GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
|
||||
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
@@ -268,6 +267,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }}
|
||||
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
|
||||
GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }}
|
||||
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
|
||||
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
||||
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
|
||||
@@ -277,6 +277,7 @@ jobs:
|
||||
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
|
||||
cd /tmp && python -m openhands.resolver.send_pull_request \
|
||||
--issue-number ${{ env.ISSUE_NUMBER }} \
|
||||
--target-branch ${{ env.TARGET_BRANCH }} \
|
||||
--pr-type draft \
|
||||
--reviewer ${{ github.actor }} | tee pr_result.txt && \
|
||||
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
|
||||
|
||||
@@ -32,6 +32,10 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install tmux
|
||||
run: sudo apt-get update && sudo apt-get install -y tmux
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
@@ -44,7 +48,7 @@ jobs:
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Run Tests
|
||||
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_memory.py
|
||||
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_long_term_memory.py
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# OpenHands Glossary
|
||||
|
||||
### Agent
|
||||
The core AI entity in OpenHands that can perform software development tasks by interacting with tools, browsing the web, and modifying code.
|
||||
|
||||
#### Agent Controller
|
||||
A component that manages the agent's lifecycle, handles its state, and coordinates interactions between the agent and various tools.
|
||||
|
||||
#### Agent Delegation
|
||||
The ability of an agent to hand off specific tasks to other specialized agents for better task completion.
|
||||
|
||||
#### Agent Hub
|
||||
A central registry of different agent types and their capabilities, allowing for easy agent selection and instantiation.
|
||||
|
||||
#### Agent Skill
|
||||
A specific capability or function that an agent can perform, such as file manipulation, web browsing, or code editing.
|
||||
|
||||
#### Agent State
|
||||
The current context and status of an agent, including its memory, active tools, and ongoing tasks.
|
||||
|
||||
#### CodeAct Agent
|
||||
[A generalist agent in OpenHands](https://arxiv.org/abs/2407.16741) designed to perform tasks by editing and executing code.
|
||||
|
||||
### Browser
|
||||
A system for web-based interactions and tasks.
|
||||
|
||||
#### Browser Gym
|
||||
A testing and evaluation environment for browser-based agent interactions and tasks.
|
||||
|
||||
#### Web Browser Tool
|
||||
A tool that enables agents to interact with web pages and perform web-based tasks.
|
||||
|
||||
### Commands
|
||||
Terminal and execution related functionality.
|
||||
|
||||
#### Bash Session
|
||||
A persistent terminal session that maintains state and history for bash command execution.
|
||||
This uses tmux under the hood.
|
||||
|
||||
### Configuration
|
||||
System-wide settings and options.
|
||||
|
||||
#### Agent Configuration
|
||||
Settings that define an agent's behavior, capabilities, and limitations, including available tools and runtime settings.
|
||||
|
||||
#### Configuration Options
|
||||
Settings that control various aspects of OpenHands behavior, including runtime, security, and agent settings.
|
||||
|
||||
#### LLM Config
|
||||
Configuration settings for language models used by agents, including model selection and parameters.
|
||||
|
||||
#### LLM Draft Config
|
||||
Settings for draft mode operations with language models, typically used for faster, lower-quality responses.
|
||||
|
||||
#### Runtime Configuration
|
||||
Settings that define how the runtime environment should be set up and operated.
|
||||
|
||||
#### Security Options
|
||||
Configuration settings that control security features and restrictions.
|
||||
|
||||
### Conversation
|
||||
A sequence of interactions between a user and an agent, including messages, actions, and their results.
|
||||
|
||||
#### Conversation Info
|
||||
Metadata about a conversation, including its status, participants, and timeline.
|
||||
|
||||
#### Conversation Manager
|
||||
A component that handles the creation, storage, and retrieval of conversations.
|
||||
|
||||
#### Conversation Metadata
|
||||
Additional information about conversations, such as tags, timestamps, and related resources.
|
||||
|
||||
#### Conversation Status
|
||||
The current state of a conversation, including whether it's active, completed, or failed.
|
||||
|
||||
#### Conversation Store
|
||||
A storage system for maintaining conversation history and related data.
|
||||
|
||||
### Events
|
||||
|
||||
#### Event
|
||||
Every Conversation comprises a series of Events. Each Event is either an Action or an Observation.
|
||||
|
||||
#### Event Stream
|
||||
A continuous flow of events that represents the ongoing activities and interactions in the system.
|
||||
|
||||
#### Action
|
||||
A specific operation or command that an agent executes through available tools, such as running a command or editing a file.
|
||||
|
||||
#### Observation
|
||||
The response or result returned by a tool after an agent's action, providing feedback about the action's outcome.
|
||||
|
||||
### Interface
|
||||
Different ways to interact with OpenHands.
|
||||
|
||||
#### CLI Mode
|
||||
A command-line interface mode for interacting with OpenHands agents without a graphical interface.
|
||||
|
||||
#### GUI Mode
|
||||
A graphical user interface mode for interacting with OpenHands agents through a web interface.
|
||||
|
||||
#### Headless Mode
|
||||
A mode of operation where OpenHands runs without a user interface, suitable for automation and scripting.
|
||||
|
||||
### Agent Memory
|
||||
The system that decides which parts of the Event Stream (i.e. the conversation history) should be passed into each LLM prompt.
|
||||
|
||||
#### Memory Store
|
||||
A storage system for maintaining agent memory and context across sessions.
|
||||
|
||||
#### Condenser
|
||||
A component that processes and summarizes conversation history to maintain context while staying within token limits.
|
||||
|
||||
#### Truncation
|
||||
A very simple Condenser strategy. Reduces conversation history or content to stay within token limits.
|
||||
|
||||
### Microagent
|
||||
A specialized prompt that enhances OpenHands with domain-specific knowledge, repository-specific context, and task-specific workflows.
|
||||
|
||||
#### Microagent Registry
|
||||
A central repository of available microagents and their configurations.
|
||||
|
||||
#### Public Microagent
|
||||
A general-purpose microagent available to all OpenHands users, triggered by specific keywords.
|
||||
|
||||
#### Repository Microagent
|
||||
A type of microagent that provides repository-specific context and guidelines, stored in the `.openhands/microagents/` directory.
|
||||
|
||||
### Prompt
|
||||
Components for managing and processing prompts.
|
||||
|
||||
#### Prompt Caching
|
||||
A system for caching and reusing common prompts to improve performance.
|
||||
|
||||
#### Prompt Manager
|
||||
A component that handles the loading, processing, and management of prompts used by agents, including microagents.
|
||||
|
||||
#### Response Parsing
|
||||
The process of interpreting and structuring responses from language models and tools.
|
||||
|
||||
### Runtime
|
||||
The execution environment where agents perform their tasks, which can be local, remote, or containerized.
|
||||
|
||||
#### Action Execution Server
|
||||
A REST API that receives agent actions (e.g. bash commands, python code, browsing actions), executes them in the runtime environment, and returns the results.
|
||||
|
||||
#### Action Execution Client
|
||||
A component that handles the execution of actions in the runtime environment, managing the communication between the agent and the runtime.
|
||||
|
||||
#### Docker Runtime
|
||||
A containerized runtime environment that provides isolation and reproducibility for agent operations.
|
||||
|
||||
#### E2B Runtime
|
||||
A specialized runtime environment built on E2B for secure and isolated code execution.
|
||||
|
||||
#### Local Runtime
|
||||
A runtime environment that executes on the local machine, suitable for development and testing.
|
||||
|
||||
#### Modal Runtime
|
||||
A runtime environment built on Modal for scalable and distributed agent operations.
|
||||
|
||||
#### Remote Runtime
|
||||
A sandboxed environment that executes code and commands remotely, providing isolation and security for agent operations.
|
||||
|
||||
#### Runtime Builder
|
||||
A component that builds a Docker image for the Action Execution Server based on a user-specified base image.
|
||||
|
||||
### Security
|
||||
Security-related components and features.
|
||||
|
||||
#### Security Analyzer
|
||||
A component that checks agent actions for potential security risks.
|
||||
+1
-1
@@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
|
||||
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.23-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.25-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -81,10 +81,10 @@ check-nodejs:
|
||||
@if command -v node > /dev/null; then \
|
||||
NODE_VERSION=$(shell node --version | sed -E 's/v//g'); \
|
||||
IFS='.' read -r -a NODE_VERSION_ARRAY <<< "$$NODE_VERSION"; \
|
||||
if [ "$${NODE_VERSION_ARRAY[0]}" -ge 20 ]; then \
|
||||
if [ "$${NODE_VERSION_ARRAY[0]}" -ge 22 ]; then \
|
||||
echo "$(BLUE)Node.js $$NODE_VERSION is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)Node.js 20.x or later is required. Please install Node.js 20.x or later to continue.$(RESET)"; \
|
||||
echo "$(RED)Node.js 22.x or later is required. Please install Node.js 22.x or later to continue.$(RESET)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
else \
|
||||
|
||||
@@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio
|
||||
system requirements and more information.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cp pyproject.toml poetry.lock openhands
|
||||
poetry build -v
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
#modal_api_token_id = ""
|
||||
#modal_api_token_secret = ""
|
||||
|
||||
# API key for Daytona
|
||||
#daytona_api_key = ""
|
||||
|
||||
# Daytona Target
|
||||
#daytona_target = ""
|
||||
|
||||
# Base path for the workspace
|
||||
workspace_base = "./workspace"
|
||||
|
||||
@@ -77,6 +83,9 @@ workspace_base = "./workspace"
|
||||
# Runtime environment
|
||||
#runtime = "docker"
|
||||
|
||||
# Runtime executor
|
||||
#runtime_executor = "openhands.runtime.executor:ActionExecutor"
|
||||
|
||||
# Name of the default agent
|
||||
#default_agent = "CodeActAgent"
|
||||
|
||||
@@ -234,6 +243,10 @@ codeact_enable_jupyter = true
|
||||
# List of microagents to disable
|
||||
#disabled_microagents = []
|
||||
|
||||
# Whether history should be truncated to continue the session when hitting LLM context
|
||||
# length limit
|
||||
enable_history_truncation = true
|
||||
|
||||
[agent.RepoExplorerAgent]
|
||||
# Example: use a cheaper model for RepoExplorerAgent to reduce cost, especially
|
||||
# useful when an agent doesn't demand high quality but uses a lot of tokens
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.23-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.25-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -24,3 +24,6 @@ inline-quotes = "single"
|
||||
|
||||
[format]
|
||||
quote-style = "single"
|
||||
|
||||
[lint.flake8-bugbear]
|
||||
extend-immutable-calls = ["Depends", "fastapi.Depends", "fastapi.params.Depends"]
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -46,3 +46,11 @@ docker run -it \
|
||||
-e THAT=that
|
||||
...
|
||||
```
|
||||
|
||||
### Referring to UI Elements
|
||||
|
||||
When referencing UI elements, use ``.
|
||||
|
||||
Example:
|
||||
1. Toggle the `Advanced` option
|
||||
2. Enter your model in the `Custom Model` textbox.
|
||||
|
||||
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -61,7 +61,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -56,6 +56,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
La façon la plus simple d'exécuter OpenHands est avec Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25
|
||||
```
|
||||
|
||||
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -59,7 +59,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -57,6 +57,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
在 Docker 中运行 OpenHands 是最简单的方式。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -54,14 +54,13 @@ graph TD
|
||||
6. Action Execution: The runtime client receives actions from the backend, executes them in the sandboxed environment, and sends back observations
|
||||
7. Observation Return: The action execution server sends execution results back to the OpenHands backend as observations
|
||||
|
||||
|
||||
The role of the client:
|
||||
|
||||
- It acts as an intermediary between the OpenHands backend and the sandboxed environment
|
||||
- It executes various types of actions (shell commands, file operations, Python code, etc.) safely within the container
|
||||
- It manages the state of the sandboxed environment, including the current working directory and loaded plugins
|
||||
- It formats and returns observations to the backend, ensuring a consistent interface for processing results
|
||||
|
||||
|
||||
## How OpenHands builds and maintains OH Runtime images
|
||||
|
||||
OpenHands' approach to building and managing runtime images ensures efficiency, consistency, and flexibility in creating and maintaining Docker images for both production and development environments.
|
||||
@@ -78,16 +77,15 @@ Tags may be in one of 2 formats:
|
||||
- **Source Tag**: `oh_v{openhands_version}_{16_digit_lock_hash}_{16_digit_source_hash}`
|
||||
(e.g.: `oh_v0.9.9_1234567890abcdef_1234567890abcdef`)
|
||||
|
||||
|
||||
#### Source Tag - Most Specific
|
||||
|
||||
This is the first 16 digits of the MD5 of the directory hash for the source directory. This gives a hash
|
||||
for only the openhands source
|
||||
|
||||
|
||||
#### Lock Tag
|
||||
|
||||
This hash is built from the first 16 digits of the MD5 of:
|
||||
|
||||
- The name of the base image upon which the image was built (e.g.: `nikolaik/python-nodejs:python3.12-nodejs22`)
|
||||
- The content of the `pyproject.toml` included in the image.
|
||||
- The content of the `poetry.lock` included in the image.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Cloud GitHub Resolver
|
||||
|
||||
The GitHub Resolver automates code fixes and provides intelligent assistance for your repositories.
|
||||
|
||||
## Setup
|
||||
|
||||
The Cloud Github Resolver is available automatically when you
|
||||
[grant OpenHands Cloud repository access](./openhands-cloud.md#adding-repositories).
|
||||
|
||||
## Usage
|
||||
|
||||
### Issues
|
||||
|
||||
On your repository, label an issue with `openhands`. OpenHands will attempt to fix the issue.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
In order to get OpenHands to work on pull requests, use `@openhands` in top level or single inline comments to:
|
||||
- Ask questions
|
||||
- Request updates
|
||||
- Get code explanations
|
||||
@@ -0,0 +1,39 @@
|
||||
# Openhands Cloud
|
||||
|
||||
OpenHands Cloud is the cloud hosted version of OpenHands by All Hands AI.
|
||||
|
||||
## Accessing OpenHands Cloud
|
||||
|
||||
Currently, users are being admitted to access OpenHands Cloud in waves. To sign up,
|
||||
[join the waitlist](https://www.all-hands.dev/join-waitlist). Once you are approved, you will get an email with
|
||||
instructions on how to access it.
|
||||
|
||||
## Getting Started
|
||||
|
||||
After visiting OpenHands Cloud, you will be asked to connect with your GitHub account:
|
||||
1. After reading and accepting the terms of service, click `Connect to GitHub`.
|
||||
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands by All Hands AI`.
|
||||
- OpenHands will require some permissions from your GitHub account. To read more about these permissions,
|
||||
you can click the `Learn more` link on the GitHub authorize page.
|
||||
|
||||
## Adding Repositories
|
||||
|
||||
You can grant OpenHands specific repository access:
|
||||
1. Under the `Select a GitHub project` dropdown, select `Add more repositories...`.
|
||||
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
|
||||
- Openhands requests short-lived tokens (8-hour expiry) with these permissions:
|
||||
- Actions: Read and write
|
||||
- Administration: Read-only
|
||||
- Commit statuses: Read and write
|
||||
- Contents: Read and write
|
||||
- Issues: Read and write
|
||||
- Metadata: Read-only
|
||||
- Pull requests: Read and write
|
||||
- Webhooks: Read and write
|
||||
- Workflows: Read and write
|
||||
- Repository access for a user is granted based on:
|
||||
- Granted permission for the repository.
|
||||
- User's GitHub permissions (owner/collaborator).
|
||||
|
||||
You can manage repository access any time by following the above workflow or visiting the Settings page and selecting
|
||||
`Configure GitHub Repositories` under the `GitHub Settings` section.
|
||||
@@ -340,6 +340,11 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
|
||||
- Default: `false`
|
||||
- Description: Whether Jupyter is enabled in the action space
|
||||
|
||||
- `enable_history_truncation`
|
||||
- Type: `bool`
|
||||
- Default: `true`
|
||||
- Description: Whether history should be truncated to continue the session when hitting LLM context length limit
|
||||
|
||||
### Microagent Usage
|
||||
- `enable_prompt_extensions`
|
||||
- Type: `bool`
|
||||
|
||||
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -45,7 +45,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -42,9 +42,10 @@ You can provide custom directions for OpenHands by following the [README for the
|
||||
Github resolver will automatically check for valid [repository secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions?tool=webui#creating-secrets-for-a-repository) or [repository variables](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) to customize its behavior.
|
||||
The customization options you can set are:
|
||||
|
||||
| **Attribute name** | **Type** | **Purpose** | **Example** |
|
||||
|----------------------------------| -------- |-------------------------------------------------------------------------------------------------------------|------------------------------------------------------|
|
||||
| `LLM_MODEL` | Variable | Set the LLM to use with OpenHands | `LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"` |
|
||||
| `OPENHANDS_MAX_ITER` | Variable | Set max limit for agent iterations | `OPENHANDS_MAX_ITER=10` |
|
||||
| `OPENHANDS_MACRO` | Variable | Customize default macro for invoking the resolver | `OPENHANDS_MACRO=@resolveit` |
|
||||
| `OPENHANDS_BASE_CONTAINER_IMAGE` | Variable | Custom Sandbox ([learn more](https://docs.all-hands.dev/modules/usage/how-to/custom-sandbox-guide)) | `OPENHANDS_BASE_CONTAINER_IMAGE="custom_image"` |
|
||||
| **Attribute name** | **Type** | **Purpose** | **Example** |
|
||||
| -------------------------------- | -------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| `LLM_MODEL` | Variable | Set the LLM to use with OpenHands | `LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"` |
|
||||
| `OPENHANDS_MAX_ITER` | Variable | Set max limit for agent iterations | `OPENHANDS_MAX_ITER=10` |
|
||||
| `OPENHANDS_MACRO` | Variable | Customize default macro for invoking the resolver | `OPENHANDS_MACRO=@resolveit` |
|
||||
| `OPENHANDS_BASE_CONTAINER_IMAGE` | Variable | Custom Sandbox ([learn more](https://docs.all-hands.dev/modules/usage/how-to/custom-sandbox-guide)) | `OPENHANDS_BASE_CONTAINER_IMAGE="custom_image"` |
|
||||
| `TARGET_BRANCH` | Variable | Merge to branch other than `main` | `TARGET_BRANCH="dev"` |
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# GUI Mode
|
||||
|
||||
## Introduction
|
||||
|
||||
OpenHands provides a user-friendly Graphical User Interface (GUI) mode for interacting with the AI assistant.
|
||||
This mode offers an intuitive way to set up the environment, manage settings, and communicate with the AI.
|
||||
OpenHands provides a Graphical User Interface (GUI) mode for interacting with the AI assistant.
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
@@ -14,104 +11,95 @@ This mode offers an intuitive way to set up the environment, manage settings, an
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. Upon first launch, you'll see a settings modal.
|
||||
2. Select an `LLM Provider` and `LLM Model` from the dropdown menus.
|
||||
1. Upon first launch, you'll see a settings page.
|
||||
2. Select an `LLM Provider` and `LLM Model` from the dropdown menus. If the required model does not exist in the list,
|
||||
toggle `Advanced` options and enter it with the correct prefix in the `Custom Model` text box.
|
||||
3. Enter the corresponding `API Key` for your chosen provider.
|
||||
4. Click "Save" to apply the settings.
|
||||
4. Click `Save Changes` to apply the settings.
|
||||
|
||||
### GitHub Token Setup
|
||||
|
||||
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
|
||||
|
||||
- **Locally (OSS)**: The user directly inputs their GitHub token.
|
||||
- **Online (SaaS)**: The token is obtained through GitHub OAuth authentication.
|
||||
|
||||
#### Setting Up a Local GitHub Token
|
||||
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
|
||||
- Click "Generate new token (classic)".
|
||||
- **Local Installation**: The user directly inputs their GitHub token.
|
||||
<details>
|
||||
<summary>Setting Up a GitHub Token</summary>
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- On GitHub, go to Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
|
||||
- Click `Generate new token (classic)`.
|
||||
- Required scopes:
|
||||
- `repo` (Full control of private repositories)
|
||||
- `workflow` (Update GitHub Action workflows)
|
||||
- `read:org` (Read organization data)
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon).
|
||||
- Navigate to the `GitHub Settings` section.
|
||||
- Paste your token in the `GitHub Token` field.
|
||||
- Click `Save Changes` to apply the changes.
|
||||
</details>
|
||||
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon) in the top right.
|
||||
- Navigate to the "GitHub" section.
|
||||
- Paste your token in the "GitHub Token" field.
|
||||
- Click "Save" to apply the changes.
|
||||
<details>
|
||||
<summary>Organizational Token Policies</summary>
|
||||
|
||||
#### Organizational Token Policies
|
||||
If you're working with organizational repositories, additional setup may be required:
|
||||
|
||||
If you're working with organizational repositories, additional setup may be required:
|
||||
|
||||
1. **Check Organization Requirements**:
|
||||
1. **Check Organization Requirements**:
|
||||
- Organization admins may enforce specific token policies.
|
||||
- Some organizations require tokens to be created with SSO enabled.
|
||||
- Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization).
|
||||
|
||||
2. **Verify Organization Access**:
|
||||
2. **Verify Organization Access**:
|
||||
- Go to your token settings on GitHub.
|
||||
- Look for the organization under "Organization access".
|
||||
- If required, click "Enable SSO" next to your organization.
|
||||
- Look for the organization under `Organization access`.
|
||||
- If required, click `Enable SSO` next to your organization.
|
||||
- Complete the SSO authorization process.
|
||||
</details>
|
||||
|
||||
#### OAuth Authentication (Online Mode)
|
||||
<details>
|
||||
<summary>Troubleshooting</summary>
|
||||
|
||||
When using OpenHands in online mode, the GitHub OAuth flow:
|
||||
Common issues and solutions:
|
||||
|
||||
1. Requests the following permissions:
|
||||
- **Token Not Recognized**:
|
||||
- Ensure the token is properly saved in settings.
|
||||
- Check that the token hasn't expired.
|
||||
- Verify the token has the required scopes.
|
||||
- Try regenerating the token.
|
||||
|
||||
- **Organization Access Denied**:
|
||||
- Check if SSO is required but not enabled.
|
||||
- Verify organization membership.
|
||||
- Contact organization admin if token policies are blocking access.
|
||||
|
||||
- **Verifying Token Works**:
|
||||
- The app will show a green checkmark if the token is valid.
|
||||
- Try accessing a repository to confirm permissions.
|
||||
- Check the browser console for any error messages.
|
||||
</details>
|
||||
|
||||
- **OpenHands Cloud**: The token is obtained through GitHub OAuth authentication.
|
||||
|
||||
<details>
|
||||
<summary>OAuth Authentication</summary>
|
||||
|
||||
When using OpenHands Cloud, the GitHub OAuth flow requests the following permissions:
|
||||
- Repository access (read/write)
|
||||
- Workflow management
|
||||
- Organization read access
|
||||
|
||||
2. Authentication steps:
|
||||
- Click "Sign in with GitHub" when prompted.
|
||||
To authenticate OpenHands:
|
||||
- Click `Sign in with GitHub` when prompted.
|
||||
- Review the requested permissions.
|
||||
- Authorize OpenHands to access your GitHub account.
|
||||
- If using an organization, authorize organization access if prompted.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
- **Token Not Recognized**:
|
||||
- Ensure the token is properly saved in settings.
|
||||
- Check that the token hasn't expired.
|
||||
- Verify the token has the required scopes.
|
||||
- Try regenerating the token.
|
||||
|
||||
- **Organization Access Denied**:
|
||||
- Check if SSO is required but not enabled.
|
||||
- Verify organization membership.
|
||||
- Contact organization admin if token policies are blocking access.
|
||||
|
||||
- **Verifying Token Works**:
|
||||
- The app will show a green checkmark if the token is valid.
|
||||
- Try accessing a repository to confirm permissions.
|
||||
- Check the browser console for any error messages.
|
||||
- Use the "Test Connection" button in settings if available.
|
||||
</details>
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
1. Toggle `Advanced Options` to access additional settings.
|
||||
1. Inside the Settings page, toggle `Advanced` options to access additional settings.
|
||||
2. Use the `Custom Model` text box to manually enter a model if it's not in the list.
|
||||
3. Specify a `Base URL` if required by your LLM provider.
|
||||
|
||||
### Main Interface
|
||||
|
||||
The main interface consists of several key components:
|
||||
|
||||
- **Chat Window**: The central area where you can view the conversation history with the AI assistant.
|
||||
- **Input Box**: Located at the bottom of the screen, use this to type your messages or commands to the AI.
|
||||
- **Send Button**: Click this to send your message to the AI.
|
||||
- **Settings Button**: A gear icon that opens the settings modal, allowing you to adjust your configuration at any time.
|
||||
- **Workspace Panel**: Displays the files and folders in your workspace, allowing you to navigate and view files, or the agent's past commands or web browsing history.
|
||||
|
||||
### Interacting with the AI
|
||||
|
||||
1. Type your question, request, or task description in the input box.
|
||||
1. Type your prompt in the input box.
|
||||
2. Click the send button or press Enter to submit your message.
|
||||
3. The AI will process your input and provide a response in the chat window.
|
||||
4. You can continue the conversation by asking follow-up questions or providing additional information.
|
||||
|
||||
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -43,7 +43,7 @@ docker run -it \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
- Linux
|
||||
- Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements)
|
||||
|
||||
A system with a modern processor and a minimum of **4GB RAM** is recommended to run OpenHands.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
<details>
|
||||
<summary>MacOS</summary>
|
||||
### Docker Desktop
|
||||
|
||||
**Docker Desktop**
|
||||
|
||||
1. [Install Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install).
|
||||
2. Open Docker Desktop, go to `Settings > Advanced` and ensure `Allow the default Docker socket to be used` is enabled.
|
||||
@@ -23,7 +26,7 @@
|
||||
Tested with Ubuntu 22.04.
|
||||
:::
|
||||
|
||||
### Docker Desktop
|
||||
**Docker Desktop**
|
||||
|
||||
1. [Install Docker Desktop on Linux](https://docs.docker.com/desktop/setup/install/linux/).
|
||||
|
||||
@@ -31,12 +34,13 @@
|
||||
|
||||
<details>
|
||||
<summary>Windows</summary>
|
||||
### WSL
|
||||
|
||||
**WSL**
|
||||
|
||||
1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
2. Run `wsl --version` in powershell and confirm `Default Version: 2`.
|
||||
|
||||
### Docker Desktop
|
||||
**Docker Desktop**
|
||||
|
||||
1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install).
|
||||
2. Open Docker Desktop, go to `Settings` and confirm the following:
|
||||
@@ -54,17 +58,17 @@
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.23
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.25
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
@@ -76,24 +80,22 @@ or run it on tagged issues with [a github action](https://docs.all-hands.dev/mod
|
||||
|
||||
## Setup
|
||||
|
||||
Upon launching OpenHands, you'll see a settings modal. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
|
||||
Upon launching OpenHands, you'll see a Settings page. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
|
||||
These can be changed at any time by selecting the `Settings` button (gear icon) in the UI.
|
||||
|
||||
If the required `LLM Model` does not exist in the list, you can toggle `Advanced Options` and manually enter it with the correct prefix
|
||||
If the required model does not exist in the list, you can toggle `Advanced` options and manually enter it with the correct prefix
|
||||
in the `Custom Model` text box.
|
||||
The `Advanced Options` also allow you to specify a `Base URL` if required.
|
||||
The `Advanced` options also allow you to specify a `Base URL` if required.
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '20px' }}>
|
||||
<img src="/img/settings-screenshot.png" alt="settings-modal" width="340" />
|
||||
<img src="/img/settings-advanced.png" alt="settings-modal" width="335" />
|
||||
</div>
|
||||
Now you're ready to [get started with OpenHands](./getting-started).
|
||||
|
||||
## Versions
|
||||
|
||||
The command above pulls the most recent stable release of OpenHands. You have other options as well:
|
||||
- For a specific release, use `docker.all-hands.dev/all-hands-ai/openhands:$VERSION`, replacing $VERSION with the version number.
|
||||
- We use semver, and release major, minor, and patch tags. So `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release.
|
||||
- For the most up-to-date development version, you can use `docker.all-hands.dev/all-hands-ai/openhands:main`. This version is unstable and is recommended for testing or development purposes only.
|
||||
The [docker command above](./installation#start-the-app) pulls the most recent stable release of OpenHands. You have other options as well:
|
||||
- For a specific release, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with the version number.
|
||||
We use SemVer so `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release.
|
||||
- For the most up-to-date development version, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with `main`.
|
||||
This version is unstable and is recommended for testing or development purposes only.
|
||||
|
||||
You can choose the tag that best suits your needs based on stability requirements and desired features.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ You will need your ChatGPT deployment name which can be found on the deployments
|
||||
<deployment-name> below.
|
||||
:::
|
||||
|
||||
1. Enable `Advanced Options`
|
||||
1. Enable `Advanced` options
|
||||
2. Set the following:
|
||||
- `Custom Model` to azure/<deployment-name>
|
||||
- `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)
|
||||
|
||||
@@ -10,7 +10,7 @@ OpenHands uses LiteLLM to make calls to Google's chat models. You can find their
|
||||
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
|
||||
- `LLM Provider` to `Gemini`
|
||||
- `LLM Model` to the model you will be using.
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`).
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`).
|
||||
- `API Key` to your Gemini API key
|
||||
|
||||
## VertexAI - Google Cloud Platform Configs
|
||||
@@ -27,4 +27,4 @@ VERTEXAI_LOCATION="<your-gcp-location>"
|
||||
Then set the following in the OpenHands UI through the Settings:
|
||||
- `LLM Provider` to `VertexAI`
|
||||
- `LLM Model` to the model you will be using.
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. vertex_ai/<model-name>).
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. vertex_ai/<model-name>).
|
||||
|
||||
@@ -8,7 +8,7 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
|
||||
- `LLM Provider` to `Groq`
|
||||
- `LLM Model` to the model you will be using. [Visit here to see the list of
|
||||
models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list, toggle
|
||||
`Advanced Options`, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`).
|
||||
`Advanced` options, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`).
|
||||
- `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys).
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ models that Groq hosts](https://console.groq.com/docs/models). If the model is n
|
||||
|
||||
The Groq endpoint for chat completion is [mostly OpenAI-compatible](https://console.groq.com/docs/openai). Therefore, you can access Groq models as you
|
||||
would access any OpenAI-compatible endpoint. In the OpenHands UI through the Settings:
|
||||
1. Enable `Advanced Options`
|
||||
1. Enable `Advanced` options
|
||||
2. Set the following:
|
||||
- `Custom Model` to the prefix `openai/` + the model you will be using (e.g. `openai/llama3-70b-8192`)
|
||||
- `Base URL` to `https://api.groq.com/openai/v1`
|
||||
|
||||
@@ -8,7 +8,7 @@ To use LiteLLM proxy with OpenHands, you need to:
|
||||
|
||||
1. Set up a LiteLLM proxy server (see [LiteLLM documentation](https://docs.litellm.ai/docs/proxy/quick_start))
|
||||
2. When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
|
||||
* Enable `Advanced Options`
|
||||
* Enable `Advanced` options
|
||||
* `Custom Model` to the prefix `litellm_proxy/` + the model you will be using (e.g. `litellm_proxy/anthropic.claude-3-5-sonnet-20241022-v2:0`)
|
||||
* `Base URL` to your LiteLLM proxy URL (e.g. `https://your-litellm-proxy.com`)
|
||||
* `API Key` to your LiteLLM proxy API key
|
||||
|
||||
@@ -38,7 +38,7 @@ The following can be set in the OpenHands UI through the Settings:
|
||||
- `LLM Provider`
|
||||
- `LLM Model`
|
||||
- `API Key`
|
||||
- `Base URL` (through `Advanced Settings`)
|
||||
- `Base URL` (through `Advanced` settings)
|
||||
|
||||
There are some settings that may be necessary for some LLMs/providers that cannot be set through the UI. Instead, these
|
||||
can be set through environment variables passed to the [docker run command](/modules/usage/installation#start-the-app)
|
||||
@@ -63,22 +63,22 @@ We have a few guides for running OpenHands with specific model providers:
|
||||
### API retries and rate limits
|
||||
|
||||
LLM providers typically have rate limits, sometimes very low, and may require retries. OpenHands will automatically
|
||||
retry requests if it receives a Rate Limit Error (429 error code), API connection error, or other transient errors.
|
||||
retry requests if it receives a Rate Limit Error (429 error code).
|
||||
|
||||
You can customize these options as you need for the provider you're using. Check their documentation, and set the
|
||||
following environment variables to control the number of retries and the time between retries:
|
||||
|
||||
- `LLM_NUM_RETRIES` (Default of 8)
|
||||
- `LLM_RETRY_MIN_WAIT` (Default of 15 seconds)
|
||||
- `LLM_RETRY_MAX_WAIT` (Default of 120 seconds)
|
||||
- `LLM_NUM_RETRIES` (Default of 4 times)
|
||||
- `LLM_RETRY_MIN_WAIT` (Default of 5 seconds)
|
||||
- `LLM_RETRY_MAX_WAIT` (Default of 30 seconds)
|
||||
- `LLM_RETRY_MULTIPLIER` (Default of 2)
|
||||
|
||||
If you are running OpenHands in development mode, you can also set these options in the `config.toml` file:
|
||||
|
||||
```toml
|
||||
[llm]
|
||||
num_retries = 8
|
||||
retry_min_wait = 15
|
||||
retry_max_wait = 120
|
||||
num_retries = 4
|
||||
retry_min_wait = 5
|
||||
retry_max_wait = 30
|
||||
retry_multiplier = 2
|
||||
```
|
||||
|
||||
@@ -8,7 +8,7 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
|
||||
* `LLM Provider` to `OpenAI`
|
||||
* `LLM Model` to the model you will be using.
|
||||
[Visit here to see a full list of OpenAI models that LiteLLM supports.](https://docs.litellm.ai/docs/providers/openai#openai-chat-completion-models)
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. openai/<model-name> like `openai/gpt-4o`).
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. openai/<model-name> like `openai/gpt-4o`).
|
||||
* `API Key` to your OpenAI API key. To find or create your OpenAI Project API Key, [see here](https://platform.openai.com/api-keys).
|
||||
|
||||
## Using OpenAI-Compatible Endpoints
|
||||
@@ -18,7 +18,7 @@ Just as for OpenAI Chat completions, we use LiteLLM for OpenAI-compatible endpoi
|
||||
## Using an OpenAI Proxy
|
||||
|
||||
If you're using an OpenAI proxy, in the OpenHands UI through the Settings:
|
||||
1. Enable `Advanced Options`
|
||||
1. Enable `Advanced` options
|
||||
2. Set the following:
|
||||
- `Custom Model` to openai/<model-name> (e.g. `openai/gpt-4o` or openai/<proxy-prefix>/<model-name>)
|
||||
- `Base URL` to the URL of your OpenAI proxy
|
||||
|
||||
@@ -8,5 +8,5 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
|
||||
* `LLM Provider` to `OpenRouter`
|
||||
* `LLM Model` to the model you will be using.
|
||||
[Visit here to see a full list of OpenRouter models](https://openrouter.ai/models).
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. openrouter/<model-name> like `openrouter/anthropic/claude-3.5-sonnet`).
|
||||
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. openrouter/<model-name> like `openrouter/anthropic/claude-3.5-sonnet`).
|
||||
* `API Key` to your OpenRouter API key.
|
||||
|
||||
@@ -16,7 +16,7 @@ some flags being passed to `docker run` that make this possible:
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
+20
-3
@@ -42,7 +42,7 @@ const sidebars: SidebarsConfig = {
|
||||
id: 'usage/prompting/microagents-public',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -66,9 +66,26 @@ const sidebars: SidebarsConfig = {
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Github Actions',
|
||||
label: 'Github Action',
|
||||
id: 'usage/how-to/github-action',
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Cloud',
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Openhands Cloud',
|
||||
id: 'usage/cloud/openhands-cloud',
|
||||
},
|
||||
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Cloud GitHub Resolver',
|
||||
id: 'usage/cloud/cloud-github-resolver',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -185,7 +202,7 @@ const sidebars: SidebarsConfig = {
|
||||
type: 'doc',
|
||||
label: 'About',
|
||||
id: 'usage/about',
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,17 @@ export default function Home(): JSX.Element {
|
||||
})}
|
||||
>
|
||||
<HomepageHeader />
|
||||
<div style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<br />
|
||||
<h2>Most Popular Links</h2>
|
||||
<ul style={{ listStyleType: 'none'}}>
|
||||
<li><a href="/modules/usage/Installation">How to Run OpenHands</a></li>
|
||||
<li><a href="/modules/usage/prompting/microagents-repo">Customizing OpenHands to a repository</a></li>
|
||||
<li><a href="/modules/usage/how-to/github-action">Integrating OpenHands with Github</a></li>
|
||||
<li><a href="/modules/usage/llms#model-recommendations">Recommended models to use</a></li>
|
||||
<li><a href="/modules/usage/runtimes#connecting-to-your-filesystem">Connecting OpenHands to your filesystem</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
@@ -20,6 +20,8 @@ To evaluate an agent, you can provide the agent's name to the `run_infer.py` pro
|
||||
### Evaluating Different LLMs
|
||||
|
||||
OpenHands in development mode uses `config.toml` to keep track of most configuration.
|
||||
**IMPORTANT: For evaluation, only the LLM section in `config.toml` will be used. Other configurations, such as `save_trajectory_path`, are not applied during evaluation.**
|
||||
|
||||
Here's an example configuration file you can use to define and use multiple LLMs:
|
||||
|
||||
```toml
|
||||
@@ -40,6 +42,8 @@ api_key = "XXX"
|
||||
temperature = 0.0
|
||||
```
|
||||
|
||||
For other configurations specific to evaluation, such as `save_trajectory_path`, these are typically set in the `get_config` function of the respective `run_infer.py` file for each benchmark.
|
||||
|
||||
## Supported Benchmarks
|
||||
|
||||
The OpenHands evaluation harness supports a wide variety of benchmarks across [software engineering](#software-engineering), [web browsing](#web-browsing), [miscellaneous assistance](#misc-assistance), and [real-world](#real-world) tasks.
|
||||
|
||||
@@ -9,6 +9,7 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -17,7 +18,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
@@ -60,17 +60,14 @@ AGENT_CLS_TO_INST_SUFFIX = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=False,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -17,6 +17,7 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -25,7 +26,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -40,21 +40,15 @@ from openhands.utils.async_utils import call_async_from_sync
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-slim'
|
||||
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-slim',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_init_timeout=3600,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -16,6 +16,7 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -24,7 +25,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
load_from_toml,
|
||||
parse_arguments,
|
||||
@@ -47,22 +47,14 @@ SKIP_NUM = (
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.11-bookworm'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.11-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
timeout=100,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_init_timeout=1800,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -14,6 +14,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -22,7 +23,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -57,18 +57,15 @@ def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
BIOCODER_BENCH_CONTAINER_IMAGE = 'public.ecr.aws/i5g0m1f6/eval_biocoder:v1.0'
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = BIOCODER_BENCH_CONTAINER_IMAGE
|
||||
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=BIOCODER_BENCH_CONTAINER_IMAGE,
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -17,6 +17,7 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -25,7 +26,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -71,17 +71,15 @@ AGENT_CLS_TO_INST_SUFFIX = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -10,6 +10,7 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -18,7 +19,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -36,17 +36,14 @@ def get_config(
|
||||
assert (
|
||||
metadata.max_iterations == 1
|
||||
), 'max_iterations must be 1 for browsing delegation evaluation.'
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=False,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
assert_and_raise,
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -25,7 +26,6 @@ from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AgentConfig,
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
@@ -105,9 +105,7 @@ def get_config(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
# COMMIT0_CONTAINER_IMAGE = 'wentingzhao/'
|
||||
assert USE_INSTANCE_IMAGE
|
||||
# We use a different instance image for the each instance of commit0 eval
|
||||
repo_name = instance['repo'].split('/')[1]
|
||||
base_container_image = get_instance_docker_image(repo_name)
|
||||
logger.info(
|
||||
@@ -115,28 +113,16 @@ def get_config(
|
||||
f'Please make sure this image exists. '
|
||||
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
|
||||
)
|
||||
# else:
|
||||
# raise
|
||||
# base_container_image = SWE_BENCH_CONTAINER_IMAGE
|
||||
# logger.info(f'Using swe-bench container image: {base_container_image}')
|
||||
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = base_container_image
|
||||
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
# large enough timeout, since some testcases take very long to run
|
||||
timeout=300,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_init_timeout=3600,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -16,6 +16,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -25,7 +26,6 @@ from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AgentConfig,
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -62,17 +62,14 @@ AGENT_CLS_TO_INST_SUFFIX = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -13,6 +13,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -21,10 +22,10 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
from openhands.core.config.utils import get_agent_config_arg
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
|
||||
@@ -47,24 +48,25 @@ AGENT_CLS_TO_INST_SUFFIX = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.enable_prompt_extensions = False
|
||||
if metadata.agent_config:
|
||||
config.set_agent_config(metadata.agent_config, metadata.agent_class)
|
||||
else:
|
||||
logger.info('Agent config not provided, using default settings')
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.enable_prompt_extensions = False
|
||||
return config
|
||||
|
||||
|
||||
@@ -238,6 +240,10 @@ if __name__ == '__main__':
|
||||
)
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
agent_config = None
|
||||
if args.agent_config:
|
||||
agent_config = get_agent_config_arg(args.agent_config)
|
||||
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
@@ -256,6 +262,7 @@ if __name__ == '__main__':
|
||||
eval_output_dir=args.eval_output_dir,
|
||||
data_split=args.data_split,
|
||||
details={'gaia-level': args.level},
|
||||
agent_config=agent_config,
|
||||
)
|
||||
|
||||
dataset = load_dataset('gaia-benchmark/GAIA', args.level)
|
||||
|
||||
@@ -9,6 +9,7 @@ AGENT=$3
|
||||
EVAL_LIMIT=$4
|
||||
LEVELS=$5
|
||||
NUM_WORKERS=$6
|
||||
AGENT_CONFIG=$7
|
||||
|
||||
if [ -z "$NUM_WORKERS" ]; then
|
||||
NUM_WORKERS=1
|
||||
@@ -49,5 +50,9 @@ if [ -n "$EVAL_LIMIT" ]; then
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
|
||||
if [ -n "$AGENT_CONFIG" ]; then
|
||||
echo "AGENT_CONFIG: $AGENT_CONFIG"
|
||||
COMMAND="$COMMAND --agent-config $AGENT_CONFIG"
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
|
||||
@@ -11,6 +11,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -19,7 +20,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
@@ -40,17 +40,14 @@ AGENT_CLS_TO_INST_SUFFIX = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -29,6 +29,7 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -37,7 +38,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
@@ -61,17 +61,14 @@ ACTION_FORMAT = """
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -22,6 +22,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -30,7 +31,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -82,17 +82,14 @@ AGENT_CLS_TO_INST_SUFFIX = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -9,6 +9,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -17,7 +18,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
@@ -45,18 +45,18 @@ AGENT_CLS_TO_INST_SUFFIX = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'xingyaoww/od-eval-logic-reasoning:v1.0'
|
||||
sandbox_config.runtime_extra_deps = (
|
||||
'$OH_INTERPRETER_PATH -m pip install scitools-pyke'
|
||||
)
|
||||
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='xingyaoww/od-eval-logic-reasoning:v1.0',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
runtime_extra_deps='$OH_INTERPRETER_PATH -m pip install scitools-pyke',
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -12,6 +12,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -21,7 +22,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -55,23 +55,14 @@ def get_config(
|
||||
metadata: EvalMetadata,
|
||||
env_id: str,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'xingyaoww/od-eval-miniwob:v1.0'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='xingyaoww/od-eval-miniwob:v1.0',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
browsergym_eval_env=env_id,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
remote_runtime_init_timeout=1800,
|
||||
keep_runtime_alive=False,
|
||||
timeout=120,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -14,6 +14,7 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -22,7 +23,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
@@ -103,18 +103,18 @@ def load_incontext_example(task_name: str, with_tool: bool = True):
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'xingyaoww/od-eval-mint:v1.0'
|
||||
sandbox_config.runtime_extra_deps = (
|
||||
f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}'
|
||||
)
|
||||
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='xingyaoww/od-eval-mint:v1.0',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
runtime_extra_deps=f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}',
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -25,6 +25,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -33,7 +34,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
load_app_config,
|
||||
@@ -77,16 +77,14 @@ ID2CONDA = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'public.ecr.aws/i5g0m1f6/ml-bench'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='public.ecr.aws/i5g0m1f6/ml-bench',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -11,6 +11,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -20,7 +21,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
@@ -59,22 +59,17 @@ def get_config(
|
||||
metadata: EvalMetadata,
|
||||
instance_id: str,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = (
|
||||
'docker.io/xingyaoww/openhands-eval-scienceagentbench'
|
||||
)
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_budget_per_task=4,
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='docker.io/xingyaoww/openhands-eval-scienceagentbench',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
timeout=300,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from functools import partial
|
||||
@@ -21,13 +22,14 @@ from evaluation.benchmarks.swe_bench.run_infer import get_instance_docker_image
|
||||
from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
get_default_sandbox_config_for_eval,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
run_evaluation,
|
||||
)
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
LLMConfig,
|
||||
get_parser,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
@@ -79,22 +81,16 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
|
||||
f'Please make sure this image exists. '
|
||||
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
|
||||
)
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = base_container_image
|
||||
sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor(
|
||||
dataset_name=metadata.dataset,
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
config = AppConfig(
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
use_host_network=False,
|
||||
# large enough timeout, since some testcases take very long to run
|
||||
timeout=600,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
remote_runtime_init_timeout=3600,
|
||||
remote_runtime_resource_factor=get_instance_resource_factor(
|
||||
dataset_name=metadata.dataset,
|
||||
instance_id=instance['instance_id'],
|
||||
),
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
@@ -415,13 +411,17 @@ if __name__ == '__main__':
|
||||
else:
|
||||
# Initialize with a dummy metadata when file doesn't exist
|
||||
metadata = EvalMetadata(
|
||||
agent_class="dummy_agent", # Placeholder agent class
|
||||
llm_config=LLMConfig(model="dummy_model"), # Minimal LLM config
|
||||
agent_class='dummy_agent', # Placeholder agent class
|
||||
llm_config=LLMConfig(model='dummy_model'), # Minimal LLM config
|
||||
max_iterations=1, # Minimal iterations
|
||||
eval_output_dir=os.path.dirname(args.input_file), # Use input file dir as output dir
|
||||
eval_output_dir=os.path.dirname(
|
||||
args.input_file
|
||||
), # Use input file dir as output dir
|
||||
start_time=time.strftime('%Y-%m-%d %H:%M:%S'), # Current time
|
||||
git_commit=subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip(), # Current commit
|
||||
dataset=args.dataset # Dataset name from args
|
||||
git_commit=subprocess.check_output(['git', 'rev-parse', 'HEAD'])
|
||||
.decode('utf-8')
|
||||
.strip(), # Current commit
|
||||
dataset=args.dataset, # Dataset name from args
|
||||
)
|
||||
|
||||
# The evaluation harness constrains the signature of `process_instance_func` but we need to
|
||||
|
||||
@@ -7,6 +7,7 @@ This file tracks the resource requirements of different instances.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"pydata__xarray-6721": 8, "pytest-dev__pytest-7236": 8, "matplotlib__matplotlib-24627": 4, "django__django-15561": 4, "django__django-15098": 4, "django__django-14771": 4, "sympy__sympy-21612": 4, "sympy__sympy-15345": 4, "psf__requests-5414": 4, "astropy__astropy-14508": 2, "django__django-11451": 2, "django__django-11477": 2, "django__django-10880": 2, "django__django-11163": 2, "django__django-11815": 2, "astropy__astropy-14369": 2, "django__django-10097": 2, "django__django-10554": 2, "django__django-12304": 2, "django__django-12325": 2, "django__django-11551": 2, "django__django-11734": 2, "django__django-13109": 2, "django__django-13089": 2, "django__django-13343": 2, "django__django-13363": 2, "django__django-13809": 2, "django__django-13810": 2, "django__django-13786": 2, "django__django-13807": 2, "django__django-14493": 2, "django__django-11820": 2, "django__django-11951": 2, "django__django-11964": 2, "astropy__astropy-14309": 2, "astropy__astropy-14365": 2, "astropy__astropy-12907": 2, "astropy__astropy-14182": 2, "django__django-15161": 2, "django__django-15128": 2, "django__django-14999": 2, "django__django-14915": 2, "django__django-14752": 2, "django__django-14765": 2, "django__django-14089": 2, "django__django-15252": 2, "django__django-15380": 2, "django__django-15382": 2, "django__django-15499": 2, "django__django-15467": 2, "django__django-15280": 2, "django__django-15315": 2, "django__django-15277": 2, "django__django-15268": 2, "django__django-15629": 2, "django__django-15695": 2, "django__django-15732": 2, "django__django-15863": 2, "django__django-16082": 2, "django__django-16145": 2, "django__django-16256": 2, "django__django-16429": 2, "django__django-16454": 2, "django__django-16493": 2, "matplotlib__matplotlib-13989": 2, "matplotlib__matplotlib-20488": 2, "django__django-15503": 2, "django__django-15525": 2, "django__django-15375": 2, "django__django-15278": 2, "matplotlib__matplotlib-21568": 2, "matplotlib__matplotlib-20859": 2, "matplotlib__matplotlib-20826": 2, "matplotlib__matplotlib-20676": 2, "matplotlib__matplotlib-23412": 2, "matplotlib__matplotlib-22719": 2, "matplotlib__matplotlib-23299": 2, "matplotlib__matplotlib-22865": 2, "matplotlib__matplotlib-24149": 2, "matplotlib__matplotlib-24177": 2, "matplotlib__matplotlib-24570": 2, "matplotlib__matplotlib-24637": 2, "matplotlib__matplotlib-24970": 2, "matplotlib__matplotlib-23476": 2, "matplotlib__matplotlib-24026": 2, "matplotlib__matplotlib-23314": 2, "matplotlib__matplotlib-25332": 2, "matplotlib__matplotlib-25311": 2, "matplotlib__matplotlib-25122": 2, "matplotlib__matplotlib-25479": 2, "matplotlib__matplotlib-26342": 2, "psf__requests-2317": 2, "matplotlib__matplotlib-25960": 2, "matplotlib__matplotlib-25775": 2, "pydata__xarray-4356": 2, "pydata__xarray-4075": 2, "pydata__xarray-6461": 2, "pydata__xarray-4687": 2, "pydata__xarray-6599": 2, "pylint-dev__pylint-4661": 2, "django__django-15554": 2, "django__django-15563": 2, "pytest-dev__pytest-5262": 2, "pytest-dev__pytest-10081": 2, "scikit-learn__scikit-learn-12973": 2, "scikit-learn__scikit-learn-13124": 2, "scikit-learn__scikit-learn-13779": 2, "scikit-learn__scikit-learn-14141": 2, "scikit-learn__scikit-learn-13439": 2, "scikit-learn__scikit-learn-13496": 2, "scikit-learn__scikit-learn-15100": 2, "scikit-learn__scikit-learn-25102": 2, "scikit-learn__scikit-learn-25232": 2, "scikit-learn__scikit-learn-25747": 2, "scikit-learn__scikit-learn-26323": 2, "scikit-learn__scikit-learn-9288": 2, "scikit-learn__scikit-learn-14496": 2, "scikit-learn__scikit-learn-14629": 2, "sphinx-doc__sphinx-8265": 2, "sphinx-doc__sphinx-8548": 2, "sphinx-doc__sphinx-8593": 2, "sphinx-doc__sphinx-8595": 2, "sphinx-doc__sphinx-8621": 2, "sphinx-doc__sphinx-8638": 2, "sphinx-doc__sphinx-9229": 2, "sphinx-doc__sphinx-9281": 2, "sphinx-doc__sphinx-9461": 2, "sphinx-doc__sphinx-9591": 2, "sphinx-doc__sphinx-9658": 2, "sphinx-doc__sphinx-9673": 2, "sympy__sympy-12096": 2, "sympy__sympy-12481": 2, "sphinx-doc__sphinx-10323": 2, "sphinx-doc__sphinx-7590": 2, "sympy__sympy-13877": 2, "sympy__sympy-12489": 2, "sympy__sympy-15809": 2, "sympy__sympy-14711": 2, "sympy__sympy-16597": 2, "sympy__sympy-16766": 2, "sympy__sympy-16792": 2, "sympy__sympy-15875": 2, "sympy__sympy-17655": 2, "sympy__sympy-18189": 2, "sympy__sympy-18763": 2, "sympy__sympy-19040": 2, "sympy__sympy-19495": 2, "sympy__sympy-19637": 2, "sympy__sympy-19783": 2, "sympy__sympy-17630": 2, "sympy__sympy-20428": 2, "sympy__sympy-20590": 2, "sympy__sympy-20801": 2, "sympy__sympy-21379": 2, "sympy__sympy-21847": 2, "sympy__sympy-22456": 2, "sympy__sympy-22714": 2, "sympy__sympy-22914": 2, "sympy__sympy-23262": 2, "sympy__sympy-23413": 2, "sympy__sympy-23534": 2, "sympy__sympy-24066": 2, "sympy__sympy-24213": 2, "sympy__sympy-24443": 2, "sympy__sympy-24562": 2, "sympy__sympy-24661": 2}
|
||||
@@ -18,6 +18,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
assert_and_raise,
|
||||
codeact_user_response,
|
||||
get_default_sandbox_config_for_eval,
|
||||
get_metrics,
|
||||
is_fatal_evaluation_error,
|
||||
make_metadata,
|
||||
@@ -30,7 +31,6 @@ from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AgentConfig,
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
@@ -122,30 +122,23 @@ def get_config(
|
||||
base_container_image = SWE_BENCH_CONTAINER_IMAGE
|
||||
logger.info(f'Using swe-bench container image: {base_container_image}')
|
||||
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = base_container_image
|
||||
sandbox_config.enable_auto_lint = True
|
||||
sandbox_config.use_host_network = False
|
||||
# Add platform to the sandbox config to solve issue 4401
|
||||
sandbox_config.platform = 'linux/amd64'
|
||||
sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor(
|
||||
dataset_name=metadata.dataset,
|
||||
instance_id=instance['instance_id'],
|
||||
)
|
||||
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
max_iterations=metadata.max_iterations,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
# large enough timeout, since some testcases take very long to run
|
||||
timeout=300,
|
||||
# Add platform to the sandbox config to solve issue 4401
|
||||
platform='linux/amd64',
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_init_timeout=3600,
|
||||
remote_runtime_api_timeout=120,
|
||||
remote_runtime_resource_factor=get_instance_resource_factor(
|
||||
dataset_name=metadata.dataset,
|
||||
instance_id=instance['instance_id'],
|
||||
),
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
@@ -331,6 +324,22 @@ def complete_runtime(
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
if obs.exit_code == -1:
|
||||
# The previous command is still running
|
||||
# We need to kill previous command
|
||||
logger.info('The previous command is still running, trying to kill it...')
|
||||
action = CmdRunAction(command='C-c')
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
# Then run the command again
|
||||
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
|
||||
action.set_hard_timeout(600)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
assert_and_raise(
|
||||
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
|
||||
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
|
||||
|
||||
@@ -267,7 +267,9 @@ def pre_login(
|
||||
obs: BrowserOutputObservation = runtime.run_action(browser_action)
|
||||
logger.debug(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
if save_screenshots:
|
||||
image_data = base64.b64decode(obs.screenshot)
|
||||
image_data = base64.b64decode(
|
||||
obs.screenshot.replace('data:image/png;base64,', '')
|
||||
)
|
||||
with open(os.path.join(directory, f'{image_id}.png'), 'wb') as file:
|
||||
file.write(image_data)
|
||||
image_id += 1
|
||||
|
||||
@@ -13,14 +13,16 @@ from typing import List
|
||||
import yaml
|
||||
from browsing import pre_login
|
||||
|
||||
from evaluation.utils.shared import get_default_sandbox_config_for_eval
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
LLMConfig,
|
||||
SandboxConfig,
|
||||
get_agent_config_arg,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
from openhands.core.config.agent_config import AgentConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.events.action import CmdRunAction, MessageAction
|
||||
@@ -34,7 +36,10 @@ def get_config(
|
||||
task_short_name: str,
|
||||
mount_path_on_host: str,
|
||||
llm_config: LLMConfig,
|
||||
agent_config: AgentConfig | None,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = base_container_image
|
||||
config = AppConfig(
|
||||
run_as_openhands=False,
|
||||
max_budget_per_task=4,
|
||||
@@ -42,22 +47,21 @@ def get_config(
|
||||
save_trajectory_path=os.path.join(
|
||||
mount_path_on_host, f'traj_{task_short_name}.json'
|
||||
),
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image=base_container_image,
|
||||
enable_auto_lint=True,
|
||||
# using host network to access the host machine from the container
|
||||
use_host_network=True,
|
||||
# large enough timeout, since some testcases take very long to run
|
||||
timeout=300,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# we mount trajectories path so that trajectories, generated by OpenHands
|
||||
# controller, can be accessible to the evaluator file in the runtime container
|
||||
workspace_mount_path=mount_path_on_host,
|
||||
workspace_mount_path_in_sandbox='/outputs',
|
||||
)
|
||||
config.set_llm_config(llm_config)
|
||||
if agent_config:
|
||||
config.set_agent_config(agent_config)
|
||||
else:
|
||||
logger.info('Agent config not provided, using default settings')
|
||||
agent_config = AgentConfig(
|
||||
enable_prompt_extensions=False,
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
return config
|
||||
|
||||
|
||||
@@ -148,11 +152,21 @@ def run_solver(
|
||||
os.makedirs(screenshots_dir, exist_ok=True)
|
||||
for image_id, obs in enumerate(state.history):
|
||||
if isinstance(obs, BrowserOutputObservation):
|
||||
image_data = base64.b64decode(obs.screenshot)
|
||||
image_data = base64.b64decode(
|
||||
obs.screenshot.replace('data:image/png;base64,', '')
|
||||
)
|
||||
with open(
|
||||
os.path.join(screenshots_dir, f'{image_id}.png'), 'wb'
|
||||
) as file:
|
||||
file.write(image_data)
|
||||
if obs.set_of_marks:
|
||||
som_image_data = base64.b64decode(
|
||||
obs.set_of_marks.replace('data:image/png;base64,', '')
|
||||
)
|
||||
with open(
|
||||
os.path.join(screenshots_dir, f'{image_id}_som.png'), 'wb'
|
||||
) as file:
|
||||
file.write(som_image_data)
|
||||
|
||||
if save_final_state:
|
||||
os.makedirs(state_dir, exist_ok=True)
|
||||
@@ -215,6 +229,10 @@ if __name__ == '__main__':
|
||||
)
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
agent_config: AgentConfig | None = None
|
||||
if args.agent_config:
|
||||
agent_config = get_agent_config_arg(args.agent_config)
|
||||
|
||||
agent_llm_config: LLMConfig | None = None
|
||||
if args.agent_llm_config:
|
||||
agent_llm_config = get_llm_config_arg(args.agent_llm_config)
|
||||
@@ -255,7 +273,7 @@ if __name__ == '__main__':
|
||||
else:
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
config: AppConfig = get_config(
|
||||
args.task_image_name, task_short_name, temp_dir, agent_llm_config
|
||||
args.task_image_name, task_short_name, temp_dir, agent_llm_config, agent_config
|
||||
)
|
||||
runtime: Runtime = create_runtime(config)
|
||||
call_async_from_sync(runtime.connect)
|
||||
|
||||
@@ -44,6 +44,10 @@ while [[ $# -gt 0 ]]; do
|
||||
ENV_LLM_CONFIG="$2"
|
||||
shift 2
|
||||
;;
|
||||
--agent-config)
|
||||
AGENT_CONFIG="$2"
|
||||
shift 2
|
||||
;;
|
||||
--outputs-path)
|
||||
OUTPUTS_PATH="$2"
|
||||
shift 2
|
||||
@@ -125,8 +129,6 @@ temp_file="tasks_${START_PERCENTILE}_${END_PERCENTILE}.md"
|
||||
sed -n "${start_line},${end_line}p" tasks.md > "$temp_file"
|
||||
|
||||
while IFS= read -r task_image; do
|
||||
docker pull $task_image
|
||||
|
||||
# Remove prefix using ## to remove longest matching pattern from start
|
||||
task_name=${task_image##ghcr.io/theagentcompany/}
|
||||
|
||||
@@ -140,13 +142,23 @@ while IFS= read -r task_image; do
|
||||
continue
|
||||
fi
|
||||
|
||||
export PYTHONPATH=evaluation/benchmarks/the_agent_company:\$PYTHONPATH && \
|
||||
poetry run python run_infer.py \
|
||||
--agent-llm-config "$AGENT_LLM_CONFIG" \
|
||||
--env-llm-config "$ENV_LLM_CONFIG" \
|
||||
--outputs-path "$OUTPUTS_PATH" \
|
||||
--server-hostname "$SERVER_HOSTNAME" \
|
||||
--task-image-name "$task_image"
|
||||
docker pull $task_image
|
||||
|
||||
# Build the Python command
|
||||
COMMAND="poetry run python run_infer.py \
|
||||
--agent-llm-config \"$AGENT_LLM_CONFIG\" \
|
||||
--env-llm-config \"$ENV_LLM_CONFIG\" \
|
||||
--outputs-path \"$OUTPUTS_PATH\" \
|
||||
--server-hostname \"$SERVER_HOSTNAME\" \
|
||||
--task-image-name \"$task_image\""
|
||||
|
||||
# Add agent-config if it's defined
|
||||
if [ -n "$AGENT_CONFIG" ]; then
|
||||
COMMAND="$COMMAND --agent-config $AGENT_CONFIG"
|
||||
fi
|
||||
|
||||
export PYTHONPATH=evaluation/benchmarks/the_agent_company:$PYTHONPATH && \
|
||||
eval "$COMMAND"
|
||||
|
||||
# Prune unused images and volumes
|
||||
docker image rm "$task_image"
|
||||
|
||||
@@ -10,6 +10,7 @@ from evaluation.utils.shared import (
|
||||
EvalOutput,
|
||||
codeact_user_response,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -18,7 +19,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
@@ -41,17 +41,14 @@ AGENT_CLS_TO_INST_SUFFIX = {
|
||||
def get_config(
|
||||
metadata: EvalMetadata,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -11,6 +11,7 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -20,7 +21,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -55,32 +55,29 @@ def get_config(
|
||||
assert base_url is not None, 'VISUALWEBARENA_BASE_URL must be set'
|
||||
assert openai_api_key is not None, 'OPENAI_API_KEY must be set'
|
||||
assert openai_base_url is not None, 'OPENAI_BASE_URL must be set'
|
||||
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
sandbox_config.browsergym_eval_env = env_id
|
||||
sandbox_config.runtime_startup_env_vars = {
|
||||
'BASE_URL': base_url,
|
||||
'OPENAI_API_KEY': openai_api_key,
|
||||
'OPENAI_BASE_URL': openai_base_url,
|
||||
'VWA_CLASSIFIEDS': f'{base_url}:9980',
|
||||
'VWA_CLASSIFIEDS_RESET_TOKEN': '4b61655535e7ed388f0d40a93600254c',
|
||||
'VWA_SHOPPING': f'{base_url}:7770',
|
||||
'VWA_SHOPPING_ADMIN': f'{base_url}:7780/admin',
|
||||
'VWA_REDDIT': f'{base_url}:9999',
|
||||
'VWA_GITLAB': f'{base_url}:8023',
|
||||
'VWA_WIKIPEDIA': f'{base_url}:8888',
|
||||
'VWA_HOMEPAGE': f'{base_url}:4399',
|
||||
}
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
browsergym_eval_env=env_id,
|
||||
runtime_startup_env_vars={
|
||||
'BASE_URL': base_url,
|
||||
'OPENAI_API_KEY': openai_api_key,
|
||||
'OPENAI_BASE_URL': openai_base_url,
|
||||
'VWA_CLASSIFIEDS': f'{base_url}:9980',
|
||||
'VWA_CLASSIFIEDS_RESET_TOKEN': '4b61655535e7ed388f0d40a93600254c',
|
||||
'VWA_SHOPPING': f'{base_url}:7770',
|
||||
'VWA_SHOPPING_ADMIN': f'{base_url}:7780/admin',
|
||||
'VWA_REDDIT': f'{base_url}:9999',
|
||||
'VWA_GITLAB': f'{base_url}:8023',
|
||||
'VWA_WIKIPEDIA': f'{base_url}:8888',
|
||||
'VWA_HOMEPAGE': f'{base_url}:4399',
|
||||
},
|
||||
timeout=300,
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -11,6 +11,7 @@ from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
compatibility_for_eval_history_pairs,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -19,7 +20,6 @@ from evaluation.utils.shared import (
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -50,29 +50,26 @@ def get_config(
|
||||
assert base_url is not None, 'WEBARENA_BASE_URL must be set'
|
||||
assert openai_api_key is not None, 'OPENAI_API_KEY must be set'
|
||||
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.base_container_image = 'python:3.12-bookworm'
|
||||
sandbox_config.browsergym_eval_env = env_id
|
||||
sandbox_config.runtime_startup_env_vars = {
|
||||
'BASE_URL': base_url,
|
||||
'OPENAI_API_KEY': openai_api_key,
|
||||
'SHOPPING': f'{base_url}:7770/',
|
||||
'SHOPPING_ADMIN': f'{base_url}:7780/admin',
|
||||
'REDDIT': f'{base_url}:9999',
|
||||
'GITLAB': f'{base_url}:8023',
|
||||
'WIKIPEDIA': f'{base_url}:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing',
|
||||
'MAP': f'{base_url}:3000',
|
||||
'HOMEPAGE': f'{base_url}:4399',
|
||||
}
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime='docker',
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
base_container_image='python:3.12-bookworm',
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
browsergym_eval_env=env_id,
|
||||
runtime_startup_env_vars={
|
||||
'BASE_URL': base_url,
|
||||
'OPENAI_API_KEY': openai_api_key,
|
||||
'SHOPPING': f'{base_url}:7770/',
|
||||
'SHOPPING_ADMIN': f'{base_url}:7780/admin',
|
||||
'REDDIT': f'{base_url}:9999',
|
||||
'GITLAB': f'{base_url}:8023',
|
||||
'WIKIPEDIA': f'{base_url}:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing',
|
||||
'MAP': f'{base_url}:3000',
|
||||
'HOMEPAGE': f'{base_url}:4399',
|
||||
},
|
||||
remote_runtime_enable_retries=True,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -8,6 +8,7 @@ from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestRes
|
||||
from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
get_default_sandbox_config_for_eval,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
@@ -21,7 +22,6 @@ from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
AgentConfig,
|
||||
AppConfig,
|
||||
SandboxConfig,
|
||||
get_llm_config_arg,
|
||||
parse_arguments,
|
||||
)
|
||||
@@ -43,23 +43,14 @@ def get_config(
|
||||
metadata: EvalMetadata,
|
||||
instance_id: str,
|
||||
) -> AppConfig:
|
||||
sandbox_config = get_default_sandbox_config_for_eval()
|
||||
sandbox_config.platform = 'linux/amd64'
|
||||
config = AppConfig(
|
||||
default_agent=metadata.agent_class,
|
||||
run_as_openhands=False,
|
||||
runtime=os.environ.get('RUNTIME', 'docker'),
|
||||
max_iterations=metadata.max_iterations,
|
||||
sandbox=SandboxConfig(
|
||||
# use default base_container_image
|
||||
enable_auto_lint=True,
|
||||
use_host_network=False,
|
||||
timeout=300,
|
||||
# Add platform to the sandbox config to solve issue 4401
|
||||
platform='linux/amd64',
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_init_timeout=3600,
|
||||
),
|
||||
sandbox=sandbox_config,
|
||||
# do not mount workspace
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
|
||||
@@ -16,7 +16,8 @@ from pydantic import BaseModel
|
||||
from tqdm import tqdm
|
||||
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.core.config import LLMConfig, SandboxConfig
|
||||
from openhands.core.config.agent_config import AgentConfig
|
||||
from openhands.core.config.condenser_config import (
|
||||
CondenserConfig,
|
||||
NoOpCondenserConfig,
|
||||
@@ -43,6 +44,7 @@ from openhands.memory.condenser import get_condensation_metadata
|
||||
class EvalMetadata(BaseModel):
|
||||
agent_class: str
|
||||
llm_config: LLMConfig
|
||||
agent_config: AgentConfig | None = None
|
||||
max_iterations: int
|
||||
eval_output_dir: str
|
||||
start_time: str
|
||||
@@ -167,6 +169,7 @@ def make_metadata(
|
||||
eval_output_dir: str,
|
||||
data_split: str | None = None,
|
||||
details: dict[str, Any] | None = None,
|
||||
agent_config: AgentConfig | None = None,
|
||||
condenser_config: CondenserConfig | None = None,
|
||||
) -> EvalMetadata:
|
||||
model_name = llm_config.model.split('/')[-1]
|
||||
@@ -189,6 +192,7 @@ def make_metadata(
|
||||
metadata = EvalMetadata(
|
||||
agent_class=agent_class,
|
||||
llm_config=llm_config,
|
||||
agent_config=agent_config,
|
||||
max_iterations=max_iterations,
|
||||
eval_output_dir=eval_output_path,
|
||||
start_time=time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
@@ -551,3 +555,18 @@ def get_metrics(state: State) -> dict[str, Any]:
|
||||
metrics = state.metrics.get() if state.metrics else {}
|
||||
metrics['condenser'] = get_condensation_metadata(state)
|
||||
return metrics
|
||||
|
||||
|
||||
def get_default_sandbox_config_for_eval() -> SandboxConfig:
|
||||
return SandboxConfig(
|
||||
use_host_network=False,
|
||||
# large enough timeout, since some testcases take very long to run
|
||||
timeout=300,
|
||||
api_key=os.environ.get('ALLHANDS_API_KEY', None),
|
||||
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
|
||||
keep_runtime_alive=False,
|
||||
remote_runtime_init_timeout=3600,
|
||||
remote_runtime_api_timeout=120,
|
||||
remote_runtime_enable_retries=True,
|
||||
remote_runtime_class='sysbox',
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { Message } from "#/message";
|
||||
import { act, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
|
||||
@@ -18,7 +18,6 @@ describe("AccountSettingsContextMenu", () => {
|
||||
it("should always render the right options", () => {
|
||||
render(
|
||||
<AccountSettingsContextMenu
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
onClose={onCloseMock}
|
||||
isLoggedIn
|
||||
@@ -28,30 +27,12 @@ describe("AccountSettingsContextMenu", () => {
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("ACCOUNT_SETTINGS$SETTINGS")).toBeInTheDocument();
|
||||
expect(screen.getByText("ACCOUNT_SETTINGS$LOGOUT")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onClickAccountSettings when the account settings option is clicked", async () => {
|
||||
render(
|
||||
<AccountSettingsContextMenu
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
onClose={onCloseMock}
|
||||
isLoggedIn
|
||||
/>,
|
||||
);
|
||||
|
||||
const accountSettingsOption = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
|
||||
await user.click(accountSettingsOption);
|
||||
|
||||
expect(onClickAccountSettingsMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should call onLogout when the logout option is clicked", async () => {
|
||||
render(
|
||||
<AccountSettingsContextMenu
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
onClose={onCloseMock}
|
||||
isLoggedIn
|
||||
@@ -67,7 +48,6 @@ describe("AccountSettingsContextMenu", () => {
|
||||
test("onLogout should be disabled if the user is not logged in", async () => {
|
||||
render(
|
||||
<AccountSettingsContextMenu
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
onClose={onCloseMock}
|
||||
isLoggedIn={false}
|
||||
@@ -83,14 +63,13 @@ describe("AccountSettingsContextMenu", () => {
|
||||
it("should call onClose when clicking outside of the element", async () => {
|
||||
render(
|
||||
<AccountSettingsContextMenu
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
onClose={onCloseMock}
|
||||
isLoggedIn
|
||||
/>,
|
||||
);
|
||||
|
||||
const accountSettingsButton = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
|
||||
const accountSettingsButton = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
|
||||
await user.click(accountSettingsButton);
|
||||
await user.click(document.body);
|
||||
|
||||
|
||||
+6
-17
@@ -1,6 +1,6 @@
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
@@ -8,7 +8,7 @@ import { SettingsProvider } from "#/context/settings-context";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
|
||||
describe("AnalyticsConsentFormModal", () => {
|
||||
it("should call saveUserSettings with default settings on confirm reset settings", async () => {
|
||||
it("should call saveUserSettings with consent", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCloseMock = vi.fn();
|
||||
const saveUserSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
@@ -26,20 +26,9 @@ describe("AnalyticsConsentFormModal", () => {
|
||||
const confirmButton = screen.getByTestId("confirm-preferences");
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(saveUserSettingsSpy).toHaveBeenCalledWith({
|
||||
user_consents_to_analytics: true,
|
||||
agent: "CodeActAgent",
|
||||
confirmation_mode: false,
|
||||
enable_default_condenser: false,
|
||||
github_token: undefined,
|
||||
language: "en",
|
||||
llm_api_key: undefined,
|
||||
llm_base_url: "",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
remote_runtime_resource_factor: 1,
|
||||
security_analyzer: "",
|
||||
unset_github_token: undefined,
|
||||
});
|
||||
expect(onCloseMock).toHaveBeenCalled();
|
||||
expect(saveUserSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ user_consents_to_analytics: true }),
|
||||
);
|
||||
await waitFor(() => expect(onCloseMock).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { PaymentForm } from "#/components/features/payment/payment-form";
|
||||
|
||||
describe("PaymentForm", () => {
|
||||
const getBalanceSpy = vi.spyOn(OpenHands, "getBalance");
|
||||
const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
const renderPaymentForm = () =>
|
||||
render(<PaymentForm />, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// useBalance hook will return the balance only if the APP_MODE is "saas"
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the users current balance", async () => {
|
||||
getBalanceSpy.mockResolvedValue("100.50");
|
||||
renderPaymentForm();
|
||||
|
||||
await waitFor(() => {
|
||||
const balance = screen.getByTestId("user-balance");
|
||||
expect(balance).toHaveTextContent("$100.50");
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the users current balance to two decimal places", async () => {
|
||||
getBalanceSpy.mockResolvedValue("100");
|
||||
renderPaymentForm();
|
||||
|
||||
await waitFor(() => {
|
||||
const balance = screen.getByTestId("user-balance");
|
||||
expect(balance).toHaveTextContent("$100.00");
|
||||
});
|
||||
});
|
||||
|
||||
test("the user can top-up a specific amount", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "50.12");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.12);
|
||||
});
|
||||
|
||||
it("should round the top-up amount to two decimal places", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "50.125456");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.13);
|
||||
});
|
||||
|
||||
it("should disable the top-up button if the user enters an invalid amount", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
expect(topUpButton).toBeDisabled();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, " ");
|
||||
|
||||
expect(topUpButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should disable the top-up button after submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "50.12");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(topUpButton).toBeDisabled();
|
||||
});
|
||||
|
||||
describe("prevent submission if", () => {
|
||||
test("user enters a negative amount", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "-50.12");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("user enters an empty string", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, " ");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("user enters a non-numeric value", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "abc");
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("user enters less than the minimum amount", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPaymentForm();
|
||||
|
||||
const topUpInput = await screen.findByTestId("top-up-input");
|
||||
await user.type(topUpInput, "9"); // test assumes the minimum is 10
|
||||
|
||||
const topUpButton = screen.getByText("Add credit");
|
||||
await user.click(topUpButton);
|
||||
|
||||
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,6 @@
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { AxiosError } from "axios";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
@@ -21,161 +18,14 @@ const renderSidebar = () =>
|
||||
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
|
||||
|
||||
describe("Sidebar", () => {
|
||||
describe("Settings", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch settings data on mount", () => {
|
||||
renderSidebar();
|
||||
expect(getSettingsSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should send all settings data when saving AI configuration", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar();
|
||||
|
||||
const settingsButton = screen.getByTestId("settings-button");
|
||||
await user.click(settingsButton);
|
||||
|
||||
const settingsModal = screen.getByTestId("ai-config-modal");
|
||||
const saveButton = within(settingsModal).getByTestId(
|
||||
"save-settings-button",
|
||||
);
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith({
|
||||
agent: "CodeActAgent",
|
||||
confirmation_mode: false,
|
||||
enable_default_condenser: false,
|
||||
language: "en",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
remote_runtime_resource_factor: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("should not reset AI configuration when saving account settings", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar();
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
const menu = screen.getByTestId("account-settings-context-menu");
|
||||
const accountSettingsButton = within(menu).getByTestId(
|
||||
"account-settings-button",
|
||||
);
|
||||
await user.click(accountSettingsButton);
|
||||
|
||||
const accountSettingsModal = screen.getByTestId("account-settings-form");
|
||||
|
||||
const languageInput =
|
||||
within(accountSettingsModal).getByLabelText(/language/i);
|
||||
await user.click(languageInput);
|
||||
|
||||
const norskOption = screen.getByText(/norsk/i);
|
||||
await user.click(norskOption);
|
||||
|
||||
const tokenInput =
|
||||
within(accountSettingsModal).getByLabelText(/GITHUB\$TOKEN_LABEL/i);
|
||||
await user.type(tokenInput, "new-token");
|
||||
|
||||
const analyticsConsentInput =
|
||||
within(accountSettingsModal).getByTestId("analytics-consent");
|
||||
await user.click(analyticsConsentInput);
|
||||
|
||||
const saveButton =
|
||||
within(accountSettingsModal).getByTestId("save-settings");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith({
|
||||
agent: "CodeActAgent",
|
||||
confirmation_mode: false,
|
||||
enable_default_condenser: false,
|
||||
github_token: "new-token",
|
||||
language: "no",
|
||||
llm_base_url: "",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
remote_runtime_resource_factor: 1,
|
||||
security_analyzer: "",
|
||||
user_consents_to_analytics: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send the api key if its SET", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar();
|
||||
|
||||
const settingsButton = screen.getByTestId("settings-button");
|
||||
await user.click(settingsButton);
|
||||
|
||||
const settingsModal = screen.getByTestId("ai-config-modal");
|
||||
|
||||
// Click the advanced options switch to show the API key input
|
||||
const advancedOptionsSwitch = within(settingsModal).getByTestId(
|
||||
"advanced-option-switch",
|
||||
);
|
||||
await user.click(advancedOptionsSwitch);
|
||||
|
||||
const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i);
|
||||
await user.type(apiKeyInput, "**********");
|
||||
|
||||
const saveButton = within(settingsModal).getByTestId(
|
||||
"save-settings-button",
|
||||
);
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith({
|
||||
agent: "CodeActAgent",
|
||||
confirmation_mode: false,
|
||||
enable_default_condenser: false,
|
||||
language: "en",
|
||||
llm_base_url: "",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
remote_runtime_resource_factor: 1,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Settings Modal", () => {
|
||||
it("should open the settings modal if the user clicks the settings button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();
|
||||
|
||||
const settingsButton = screen.getByTestId("settings-button");
|
||||
await user.click(settingsButton);
|
||||
|
||||
const settingsModal = screen.getByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open the settings modal if GET /settings fails with a 404", async () => {
|
||||
const error = new AxiosError(
|
||||
"Request failed with status code 404",
|
||||
"ERR_BAD_REQUEST",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
data: { message: "Settings not found" },
|
||||
headers: {},
|
||||
// @ts-expect-error - we only need the response object for this test
|
||||
config: {},
|
||||
},
|
||||
);
|
||||
|
||||
vi.spyOn(OpenHands, "getSettings").mockRejectedValue(error);
|
||||
|
||||
renderSidebar();
|
||||
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
});
|
||||
it("should fetch settings data on mount", () => {
|
||||
renderSidebar();
|
||||
expect(getSettingsSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Messages } from "#/components/features/chat/messages";
|
||||
import type { Message } from "#/message";
|
||||
|
||||
describe("File Operations Messages", () => {
|
||||
it("should show success indicator for successful file read operation", () => {
|
||||
const messages: Message[] = [
|
||||
{
|
||||
type: "action",
|
||||
translationID: "read_file_contents",
|
||||
content: "Successfully read file contents",
|
||||
success: true,
|
||||
sender: "assistant",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
render(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
|
||||
|
||||
const statusIcon = screen.getByTestId("status-icon");
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
expect(statusIcon.closest("svg")).toHaveClass("fill-success");
|
||||
});
|
||||
|
||||
it("should show failure indicator for failed file read operation", () => {
|
||||
const messages: Message[] = [
|
||||
{
|
||||
type: "action",
|
||||
translationID: "read_file_contents",
|
||||
content: "Failed to read file contents",
|
||||
success: false,
|
||||
sender: "assistant",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
render(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
|
||||
|
||||
const statusIcon = screen.getByTestId("status-icon");
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
expect(statusIcon.closest("svg")).toHaveClass("fill-danger");
|
||||
});
|
||||
|
||||
it("should show success indicator for successful file edit operation", () => {
|
||||
const messages: Message[] = [
|
||||
{
|
||||
type: "action",
|
||||
translationID: "edit_file_contents",
|
||||
content: "Successfully edited file contents",
|
||||
success: true,
|
||||
sender: "assistant",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
render(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
|
||||
|
||||
const statusIcon = screen.getByTestId("status-icon");
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
expect(statusIcon.closest("svg")).toHaveClass("fill-success");
|
||||
});
|
||||
|
||||
it("should show failure indicator for failed file edit operation", () => {
|
||||
const messages: Message[] = [
|
||||
{
|
||||
type: "action",
|
||||
translationID: "edit_file_contents",
|
||||
content: "Failed to edit file contents",
|
||||
success: false,
|
||||
sender: "assistant",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
render(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
|
||||
|
||||
const statusIcon = screen.getByTestId("status-icon");
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
expect(statusIcon.closest("svg")).toHaveClass("fill-danger");
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import translations from "../../src/i18n/translation.json";
|
||||
import { UserAvatar } from "../../src/components/features/sidebar/user-avatar";
|
||||
|
||||
vi.mock("@nextui-org/react", () => ({
|
||||
vi.mock("@heroui/react", () => ({
|
||||
Tooltip: ({ content, children }: { content: string; children: React.ReactNode }) => (
|
||||
<div>
|
||||
{children}
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { AccountSettingsModal } from "#/components/shared/modals/account-settings/account-settings-modal";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import * as ConsentHandlers from "#/utils/handle-capture-consent";
|
||||
|
||||
describe("AccountSettingsModal", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it.skip("should set the appropriate user analytics consent default", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
user_consents_to_analytics: true,
|
||||
});
|
||||
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
|
||||
|
||||
const analyticsConsentInput = screen.getByTestId("analytics-consent");
|
||||
await waitFor(() => expect(analyticsConsentInput).toBeChecked());
|
||||
});
|
||||
|
||||
it("should save the users consent to analytics when saving account settings", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
|
||||
|
||||
const analyticsConsentInput = screen.getByTestId("analytics-consent");
|
||||
await user.click(analyticsConsentInput);
|
||||
|
||||
const saveButton = screen.getByTestId("save-settings");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith({
|
||||
agent: "CodeActAgent",
|
||||
confirmation_mode: false,
|
||||
enable_default_condenser: false,
|
||||
language: "en",
|
||||
llm_base_url: "",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
remote_runtime_resource_factor: 1,
|
||||
security_analyzer: "",
|
||||
user_consents_to_analytics: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should call handleCaptureConsent with the analytics consent value if the save is successful", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleCaptureConsentSpy = vi.spyOn(
|
||||
ConsentHandlers,
|
||||
"handleCaptureConsent",
|
||||
);
|
||||
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
|
||||
|
||||
const analyticsConsentInput = screen.getByTestId("analytics-consent");
|
||||
await user.click(analyticsConsentInput);
|
||||
|
||||
const saveButton = screen.getByTestId("save-settings");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
||||
|
||||
await user.click(analyticsConsentInput);
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("should send all settings data when saving account settings", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
|
||||
|
||||
const languageInput = screen.getByLabelText(/language/i);
|
||||
await user.click(languageInput);
|
||||
|
||||
const norskOption = screen.getByText(/norsk/i);
|
||||
await user.click(norskOption);
|
||||
|
||||
const tokenInput = screen.getByTestId("github-token-input");
|
||||
await user.type(tokenInput, "new-token");
|
||||
|
||||
const saveButton = screen.getByTestId("save-settings");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith({
|
||||
agent: "CodeActAgent",
|
||||
confirmation_mode: false,
|
||||
enable_default_condenser: false,
|
||||
language: "no",
|
||||
github_token: "new-token",
|
||||
llm_base_url: "",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
remote_runtime_resource_factor: 1,
|
||||
security_analyzer: "",
|
||||
user_consents_to_analytics: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should render a checkmark and not the input if the github token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const checkmark = screen.queryByTestId("github-token-set-checkmark");
|
||||
const input = screen.queryByTestId("github-token-input");
|
||||
|
||||
expect(checkmark).toBeInTheDocument();
|
||||
expect(input).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an unset github token property when pressing disconnect", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
|
||||
|
||||
const disconnectButton = await screen.findByTestId("disconnect-github");
|
||||
await user.click(disconnectButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith({
|
||||
agent: "CodeActAgent",
|
||||
confirmation_mode: false,
|
||||
enable_default_condenser: false,
|
||||
language: "en",
|
||||
llm_base_url: "",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
remote_runtime_resource_factor: 1,
|
||||
security_analyzer: "",
|
||||
unset_github_token: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should not unset the github token when changing the language", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
|
||||
|
||||
const languageInput = screen.getByLabelText(/language/i);
|
||||
await user.click(languageInput);
|
||||
|
||||
const norskOption = screen.getByText(/norsk/i);
|
||||
await user.click(norskOption);
|
||||
|
||||
const saveButton = screen.getByTestId("save-settings");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith({
|
||||
agent: "CodeActAgent",
|
||||
confirmation_mode: false,
|
||||
enable_default_condenser: false,
|
||||
language: "no",
|
||||
llm_base_url: "",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
remote_runtime_resource_factor: 1,
|
||||
security_analyzer: "",
|
||||
user_consents_to_analytics: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
|
||||
describe("BrandButton", () => {
|
||||
const onClickMock = vi.fn();
|
||||
|
||||
it("should set a test id", () => {
|
||||
render(
|
||||
<BrandButton testId="brand-button" type="button" variant="primary">
|
||||
Test Button
|
||||
</BrandButton>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("brand-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onClick when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<BrandButton type="button" variant="primary" onClick={onClickMock}>
|
||||
Test Button
|
||||
</BrandButton>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Test Button"));
|
||||
});
|
||||
|
||||
it("should be disabled if isDisabled is true", () => {
|
||||
render(
|
||||
<BrandButton type="button" variant="primary" isDisabled>
|
||||
Test Button
|
||||
</BrandButton>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Test Button")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { ModelSelector } from "#/components/shared/modals/settings/model-selector";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||
|
||||
describe("SettingsInput", () => {
|
||||
it("should render an optional tag if showOptionalTag is true", async () => {
|
||||
const { rerender } = render(
|
||||
<SettingsInput testId="test-input" label="Test Input" type="text" />,
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/optional/i)).not.toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<SettingsInput
|
||||
testId="test-input"
|
||||
showOptionalTag
|
||||
label="Test Input"
|
||||
type="text"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/optional/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should disable the input if isDisabled is true", async () => {
|
||||
const { rerender } = render(
|
||||
<SettingsInput testId="test-input" label="Test Input" type="text" />,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("test-input")).toBeEnabled();
|
||||
|
||||
rerender(
|
||||
<SettingsInput
|
||||
testId="test-input"
|
||||
label="Test Input"
|
||||
type="text"
|
||||
isDisabled
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("test-input")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should set a placeholder on the input", async () => {
|
||||
render(
|
||||
<SettingsInput
|
||||
testId="test-input"
|
||||
label="Test Input"
|
||||
type="text"
|
||||
placeholder="Test Placeholder"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("test-input")).toHaveAttribute(
|
||||
"placeholder",
|
||||
"Test Placeholder",
|
||||
);
|
||||
});
|
||||
|
||||
it("should set a default value on the input", async () => {
|
||||
render(
|
||||
<SettingsInput
|
||||
testId="test-input"
|
||||
label="Test Input"
|
||||
type="text"
|
||||
defaultValue="Test Value"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("test-input")).toHaveValue("Test Value");
|
||||
});
|
||||
|
||||
it("should render start content", async () => {
|
||||
const startContent = <div>Start Content</div>;
|
||||
|
||||
render(
|
||||
<SettingsInput
|
||||
testId="test-input"
|
||||
label="Test Input"
|
||||
type="text"
|
||||
defaultValue="Test Value"
|
||||
startContent={startContent}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Start Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onChange with the input value", async () => {
|
||||
const onChangeMock = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<SettingsInput
|
||||
testId="test-input"
|
||||
label="Test Input"
|
||||
type="text"
|
||||
onChange={onChangeMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId("test-input");
|
||||
await user.type(input, "Test");
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledTimes(4);
|
||||
expect(onChangeMock).toHaveBeenNthCalledWith(4, "Test");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SettingsSwitch } from "#/components/features/settings/settings-switch";
|
||||
|
||||
describe("SettingsSwitch", () => {
|
||||
it("should call the onChange handler when the input is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onToggleMock = vi.fn();
|
||||
render(
|
||||
<SettingsSwitch testId="test-switch" onToggle={onToggleMock}>
|
||||
Test Switch
|
||||
</SettingsSwitch>,
|
||||
);
|
||||
|
||||
const switchInput = screen.getByTestId("test-switch");
|
||||
|
||||
await user.click(switchInput);
|
||||
expect(onToggleMock).toHaveBeenCalledWith(true);
|
||||
|
||||
await user.click(switchInput);
|
||||
expect(onToggleMock).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("should render a beta tag if isBeta is true", () => {
|
||||
const { rerender } = render(
|
||||
<SettingsSwitch testId="test-switch" onToggle={vi.fn()} isBeta={false}>
|
||||
Test Switch
|
||||
</SettingsSwitch>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/beta/i)).not.toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<SettingsSwitch testId="test-switch" onToggle={vi.fn()} isBeta>
|
||||
Test Switch
|
||||
</SettingsSwitch>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/beta/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should be able to set a default toggle state", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onToggleMock = vi.fn();
|
||||
render(
|
||||
<SettingsSwitch
|
||||
testId="test-switch"
|
||||
onToggle={onToggleMock}
|
||||
defaultIsToggled
|
||||
>
|
||||
Test Switch
|
||||
</SettingsSwitch>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("test-switch")).toBeChecked();
|
||||
|
||||
const switchInput = screen.getByTestId("test-switch");
|
||||
await user.click(switchInput);
|
||||
expect(onToggleMock).toHaveBeenCalledWith(false);
|
||||
|
||||
expect(screen.getByTestId("test-switch")).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
@@ -1,36 +1,22 @@
|
||||
import { screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
|
||||
import { screen } from "@testing-library/react";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
describe("SettingsForm", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const onCloseMock = vi.fn();
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
const onCloseMock = vi.fn();
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "123",
|
||||
});
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
const RouteStub = createRoutesStub([
|
||||
{
|
||||
Component: () => (
|
||||
<SettingsForm
|
||||
settings={DEFAULT_SETTINGS}
|
||||
models={["anthropic/claude-3-5-sonnet-20241022", "model2"]}
|
||||
agents={["CodeActAgent", "agent2"]}
|
||||
securityAnalyzers={["analyzer1", "analyzer2"]}
|
||||
models={[DEFAULT_SETTINGS.LLM_MODEL]}
|
||||
onClose={onCloseMock}
|
||||
/>
|
||||
),
|
||||
@@ -38,39 +24,17 @@ describe("SettingsForm", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
it("should not show runtime size selector by default", () => {
|
||||
renderWithProviders(<RouterStub />);
|
||||
expect(screen.queryByText("Runtime Size")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show runtime size selector when advanced options are enabled", async () => {
|
||||
it("should save the user settings and close the modal when the form is submitted", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub />);
|
||||
renderWithProviders(<RouteStub />);
|
||||
|
||||
const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
|
||||
await user.click(toggleAdvancedMode);
|
||||
|
||||
await screen.findByTestId("runtime-size");
|
||||
});
|
||||
|
||||
it("should not submit the form if required fields are empty", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub />);
|
||||
|
||||
expect(screen.queryByTestId("custom-model-input")).not.toBeInTheDocument();
|
||||
|
||||
const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
|
||||
await user.click(toggleAdvancedMode);
|
||||
|
||||
const customModelInput = screen.getByTestId("custom-model-input");
|
||||
expect(customModelInput).toBeInTheDocument();
|
||||
|
||||
await user.clear(customModelInput);
|
||||
|
||||
const saveButton = screen.getByTestId("save-settings-button");
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).not.toHaveBeenCalled();
|
||||
expect(onCloseMock).not.toHaveBeenCalled();
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,24 +14,14 @@ describe("UserActions", () => {
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
render(
|
||||
<UserActions
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
/>,
|
||||
);
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
expect(screen.getByTestId("user-actions")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should toggle the user menu when the user avatar is clicked", async () => {
|
||||
render(
|
||||
<UserActions
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
/>,
|
||||
);
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
@@ -47,30 +37,9 @@ describe("UserActions", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onClickAccountSettings and close the menu when the account settings option is clicked", async () => {
|
||||
render(
|
||||
<UserActions
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
/>,
|
||||
);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
const accountSettingsOption = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
|
||||
await user.click(accountSettingsOption);
|
||||
|
||||
expect(onClickAccountSettingsMock).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onLogout and close the menu when the logout option is clicked", async () => {
|
||||
render(
|
||||
<UserActions
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
/>,
|
||||
@@ -89,12 +58,7 @@ describe("UserActions", () => {
|
||||
});
|
||||
|
||||
test("onLogout should not be called when the user is not logged in", async () => {
|
||||
render(
|
||||
<UserActions
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
/>,
|
||||
);
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
@@ -104,21 +68,4 @@ describe("UserActions", () => {
|
||||
|
||||
expect(onLogoutMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// FIXME: Spinner now provided through useQuery
|
||||
it.skip("should display the loading spinner", () => {
|
||||
render(
|
||||
<UserActions
|
||||
onClickAccountSettings={onClickAccountSettingsMock}
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
user.click(userAvatar);
|
||||
|
||||
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
|
||||
expect(screen.queryByAltText("user avatar")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
|
||||
describe("useSaveSettings", () => {
|
||||
it("should send an empty string for llm_api_key if an empty string is passed, otherwise undefined", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const { result } = renderHook(() => useSaveSettings(), {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
result.current.mutate({ LLM_API_KEY: "" });
|
||||
await waitFor(() => {
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_api_key: "",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
result.current.mutate({ LLM_API_KEY: null });
|
||||
await waitFor(() => {
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_api_key: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,21 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import i18n from '../../src/i18n';
|
||||
import { AccountSettingsContextMenu } from '../../src/components/features/context-menu/account-settings-context-menu';
|
||||
import { renderWithProviders } from '../../test-utils';
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import i18n from "../../src/i18n";
|
||||
import { AccountSettingsContextMenu } from "../../src/components/features/context-menu/account-settings-context-menu";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
|
||||
describe('Translations', () => {
|
||||
it('should render translated text', () => {
|
||||
i18n.changeLanguage('en');
|
||||
describe("Translations", () => {
|
||||
it("should render translated text", () => {
|
||||
i18n.changeLanguage("en");
|
||||
renderWithProviders(
|
||||
<AccountSettingsContextMenu
|
||||
onClickAccountSettings={() => {}}
|
||||
onLogout={() => {}}
|
||||
onClose={() => {}}
|
||||
isLoggedIn={true}
|
||||
/>
|
||||
isLoggedIn
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId('account-settings-context-menu')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import { screen, waitFor } from "@testing-library/react";
|
||||
import toast from "react-hot-toast";
|
||||
import App from "#/routes/_oh.app/route";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
|
||||
|
||||
describe("App", () => {
|
||||
const RouteStub = createRoutesStub([
|
||||
@@ -35,7 +34,7 @@ describe("App", () => {
|
||||
await screen.findByTestId("app-route");
|
||||
});
|
||||
|
||||
it.skipIf(!MULTI_CONVERSATION_UI)(
|
||||
it(
|
||||
"should call endSession if the user does not have permission to view conversation",
|
||||
async () => {
|
||||
const errorToastSpy = vi.spyOn(toast, "error");
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { AxiosError } from "axios";
|
||||
import MainApp from "#/routes/_oh/route";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import Home from "#/routes/_oh._index/route";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
"Request failed with status code 404",
|
||||
"ERR_BAD_REQUEST",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
data: { message: "Settings not found" },
|
||||
headers: {},
|
||||
// @ts-expect-error - we only need the response object for this test
|
||||
config: {},
|
||||
},
|
||||
);
|
||||
|
||||
describe("Home Screen", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
// layout route
|
||||
Component: MainApp,
|
||||
path: "/",
|
||||
children: [
|
||||
{
|
||||
// home route
|
||||
Component: Home,
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
Component: SettingsScreen,
|
||||
path: "/settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the home screen", () => {
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
});
|
||||
|
||||
it("should navigate to the settings screen when the settings button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const settingsButton = await screen.findByTestId("settings-button");
|
||||
await user.click(settingsButton);
|
||||
|
||||
const settingsScreen = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreen).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should navigate to the settings when pressing 'Connect to GitHub' if the user isn't authenticated", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const connectToGitHubButton =
|
||||
await screen.findByTestId("connect-to-github");
|
||||
await user.click(connectToGitHubButton);
|
||||
|
||||
const settingsScreen = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreen).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Settings 404", () => {
|
||||
it("should open the settings modal if GET /settings fails with a 404", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should navigate to the settings screen when clicking the advanced settings button", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
getSettingsSpy.mockRejectedValue(error);
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterStub initialEntries={["/"]} />);
|
||||
|
||||
const settingsScreen = screen.queryByTestId("settings-screen");
|
||||
expect(settingsScreen).not.toBeInTheDocument();
|
||||
|
||||
const settingsModal = await screen.findByTestId("ai-config-modal");
|
||||
expect(settingsModal).toBeInTheDocument();
|
||||
|
||||
const advancedSettingsButton = await screen.findByTestId(
|
||||
"advanced-settings-link",
|
||||
);
|
||||
await user.click(advancedSettingsButton);
|
||||
|
||||
const settingsScreenAfter = await screen.findByTestId("settings-screen");
|
||||
expect(settingsScreenAfter).toBeInTheDocument();
|
||||
|
||||
const settingsModalAfter = screen.queryByTestId("ai-config-modal");
|
||||
expect(settingsModalAfter).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import { PaymentForm } from "#/components/features/payment/payment-form";
|
||||
import * as FeatureFlags from "#/utils/feature-flags";
|
||||
|
||||
describe("Settings Billing", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true);
|
||||
|
||||
const RoutesStub = createRoutesStub([
|
||||
{
|
||||
Component: SettingsScreen,
|
||||
path: "/settings",
|
||||
children: [
|
||||
{
|
||||
Component: () => <PaymentForm />,
|
||||
path: "/settings/billing",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const renderSettingsScreen = () =>
|
||||
renderWithProviders(<RoutesStub initialEntries={["/settings"]} />);
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should not render the navbar if OSS mode", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const navbar = screen.queryByTestId("settings-navbar");
|
||||
expect(navbar).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the navbar if SaaS mode", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const navbar = screen.getByTestId("settings-navbar");
|
||||
within(navbar).getByText("Account");
|
||||
within(navbar).getByText("Credits");
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the billing settings if clicking the credits item", async () => {
|
||||
const user = userEvent.setup();
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const navbar = await screen.findByTestId("settings-navbar");
|
||||
const credits = within(navbar).getByText("Credits");
|
||||
await user.click(credits);
|
||||
|
||||
const billingSection = await screen.findByTestId("billing-settings");
|
||||
within(billingSection).getByText("Manage Credits");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,956 @@
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { afterEach, describe, expect, it, test, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import userEvent, { UserEvent } from "@testing-library/user-event";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { PostApiSettings } from "#/types/settings";
|
||||
import * as ConsentHandlers from "#/utils/handle-capture-consent";
|
||||
import AccountSettings from "#/routes/account-settings";
|
||||
|
||||
const toggleAdvancedSettings = async (user: UserEvent) => {
|
||||
const advancedSwitch = await screen.findByTestId("advanced-settings-switch");
|
||||
await user.click(advancedSwitch);
|
||||
};
|
||||
|
||||
describe("Settings Screen", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
const { handleLogoutMock } = vi.hoisted(() => ({
|
||||
handleLogoutMock: vi.fn(),
|
||||
}));
|
||||
vi.mock("#/hooks/use-app-logout", () => ({
|
||||
useAppLogout: vi.fn().mockReturnValue({ handleLogout: handleLogoutMock }),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: SettingsScreen,
|
||||
path: "/settings",
|
||||
children: [{ Component: AccountSettings, path: "/settings" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const renderSettingsScreen = () => {
|
||||
const queryClient = new QueryClient();
|
||||
return render(<RouterStub initialEntries={["/settings"]} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
it("should render", async () => {
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText("LLM Settings");
|
||||
screen.getByText("GitHub Settings");
|
||||
screen.getByText("Additional Settings");
|
||||
screen.getByText("Reset to defaults");
|
||||
screen.getByText("Save Changes");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Account Settings", () => {
|
||||
it("should render the account settings", async () => {
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByTestId("github-token-input");
|
||||
screen.getByTestId("github-token-help-anchor");
|
||||
screen.getByTestId("language-input");
|
||||
screen.getByTestId("enable-analytics-switch");
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Set a better unset indicator
|
||||
it.skip("should render an indicator if the GitHub token is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: false,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const input = screen.getByTestId("github-token-input");
|
||||
const inputParent = input.parentElement;
|
||||
|
||||
if (inputParent) {
|
||||
const badge = within(inputParent).getByTestId("unset-indicator");
|
||||
expect(badge).toBeInTheDocument();
|
||||
} else {
|
||||
throw new Error("GitHub token input parent not found");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should set asterik placeholder if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const input = screen.getByTestId("github-token-input");
|
||||
expect(input).toHaveProperty("placeholder", "**********");
|
||||
});
|
||||
});
|
||||
|
||||
it("should render an indicator if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const input = await screen.findByTestId("github-token-input");
|
||||
const inputParent = input.parentElement;
|
||||
|
||||
if (inputParent) {
|
||||
const badge = await within(inputParent).findByTestId("set-indicator");
|
||||
expect(badge).toBeInTheDocument();
|
||||
} else {
|
||||
throw new Error("GitHub token input parent not found");
|
||||
}
|
||||
});
|
||||
|
||||
it("should render a disabled 'Disconnect from GitHub' button if the GitHub token is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: false,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const button = await screen.findByText("Disconnect from GitHub");
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should render an enabled 'Disconnect from GitHub' button if the GitHub token is set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
const button = await screen.findByText("Disconnect from GitHub");
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeEnabled();
|
||||
|
||||
// input should still be rendered
|
||||
const input = await screen.findByTestId("github-token-input");
|
||||
expect(input).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should logout the user when the 'Disconnect from GitHub' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const button = await screen.findByText("Disconnect from GitHub");
|
||||
await user.click(button);
|
||||
|
||||
expect(handleLogoutMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not render the 'Configure GitHub Repositories' button if OSS mode", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const button = screen.queryByText("Configure GitHub Repositories");
|
||||
expect(button).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the 'Configure GitHub Repositories' button if SaaS mode and app slug exists", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
APP_SLUG: "test-app",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
await screen.findByText("Configure GitHub Repositories");
|
||||
});
|
||||
|
||||
it("should not render the GitHub token input if SaaS mode", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const input = screen.queryByTestId("github-token-input");
|
||||
const helpAnchor = screen.queryByTestId("github-token-help-anchor");
|
||||
|
||||
expect(input).not.toBeInTheDocument();
|
||||
expect(helpAnchor).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("should not reset LLM Provider and Model if GitHub token is invalid", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
github_token_is_set: false,
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
});
|
||||
saveSettingsSpy.mockRejectedValueOnce(new Error("Invalid GitHub token"));
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
let llmProviderInput = await screen.findByTestId("llm-provider-input");
|
||||
let llmModelInput = await screen.findByTestId("llm-model-input");
|
||||
|
||||
expect(llmProviderInput).toHaveValue("Anthropic");
|
||||
expect(llmModelInput).toHaveValue("claude-3-5-sonnet-20241022");
|
||||
|
||||
const input = await screen.findByTestId("github-token-input");
|
||||
await user.type(input, "invalid-token");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
llmProviderInput = await screen.findByTestId("llm-provider-input");
|
||||
llmModelInput = await screen.findByTestId("llm-model-input");
|
||||
|
||||
expect(llmProviderInput).toHaveValue("Anthropic");
|
||||
expect(llmModelInput).toHaveValue("claude-3-5-sonnet-20241022");
|
||||
});
|
||||
|
||||
test("enabling advanced, enabling confirmation mode, and then disabling + enabling advanced should not render the security analyzer input", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
const confirmationModeSwitch = await screen.findByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
await user.click(confirmationModeSwitch);
|
||||
|
||||
let securityAnalyzerInput = screen.queryByTestId(
|
||||
"security-analyzer-input",
|
||||
);
|
||||
expect(securityAnalyzerInput).toBeInTheDocument();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
securityAnalyzerInput = screen.queryByTestId("security-analyzer-input");
|
||||
expect(securityAnalyzerInput).not.toBeInTheDocument();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
securityAnalyzerInput = screen.queryByTestId("security-analyzer-input");
|
||||
expect(securityAnalyzerInput).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("LLM Settings", () => {
|
||||
it("should render the basic LLM settings by default", async () => {
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByTestId("advanced-settings-switch");
|
||||
screen.getByTestId("llm-provider-input");
|
||||
screen.getByTestId("llm-model-input");
|
||||
screen.getByTestId("llm-api-key-input");
|
||||
screen.getByTestId("llm-api-key-help-anchor");
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the advanced LLM settings if the advanced switch is toggled", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
// Should not render the advanced settings by default
|
||||
expect(
|
||||
screen.queryByTestId("llm-custom-model-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("base-url-input")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("agent-input")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("enable-confirmation-mode-switch"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const advancedSwitch = await screen.findByTestId(
|
||||
"advanced-settings-switch",
|
||||
);
|
||||
await user.click(advancedSwitch);
|
||||
|
||||
// Should render the advanced settings
|
||||
expect(
|
||||
screen.queryByTestId("llm-provider-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("llm-model-input")).not.toBeInTheDocument();
|
||||
|
||||
screen.getByTestId("llm-custom-model-input");
|
||||
screen.getByTestId("base-url-input");
|
||||
screen.getByTestId("agent-input");
|
||||
|
||||
// "Invariant" security analyzer
|
||||
screen.getByTestId("enable-confirmation-mode-switch");
|
||||
|
||||
// Not rendered until the switch is toggled
|
||||
// screen.getByTestId("security-analyzer-input");
|
||||
});
|
||||
|
||||
// TODO: Set a better unset indicator
|
||||
it.skip("should render an indicator if the LLM API key is not set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key: null,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const input = screen.getByTestId("llm-api-key-input");
|
||||
const inputParent = input.parentElement;
|
||||
|
||||
if (inputParent) {
|
||||
const badge = within(inputParent).getByTestId("unset-indicator");
|
||||
expect(badge).toBeInTheDocument();
|
||||
} else {
|
||||
throw new Error("LLM API Key input parent not found");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should render an indicator if the LLM API key is set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key: "**********",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const input = screen.getByTestId("llm-api-key-input");
|
||||
const inputParent = input.parentElement;
|
||||
|
||||
if (inputParent) {
|
||||
const badge = within(inputParent).getByTestId("set-indicator");
|
||||
expect(badge).toBeInTheDocument();
|
||||
} else {
|
||||
throw new Error("LLM API Key input parent not found");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should set asterik placeholder if the LLM API key is set", async () => {
|
||||
getSettingsSpy.mockResolvedValueOnce({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key: "**********",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const input = screen.getByTestId("llm-api-key-input");
|
||||
expect(input).toHaveProperty("placeholder", "**********");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Basic Model Selector", () => {
|
||||
it("should set the provider and model", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const providerInput = screen.getByTestId("llm-provider-input");
|
||||
const modelInput = screen.getByTestId("llm-model-input");
|
||||
|
||||
expect(providerInput).toHaveValue("Anthropic");
|
||||
expect(modelInput).toHaveValue("claude-3-5-sonnet-20241022");
|
||||
});
|
||||
});
|
||||
|
||||
it.todo("should change the model values if the provider is changed");
|
||||
|
||||
it.todo("should clear the model values if the provider is cleared");
|
||||
});
|
||||
|
||||
describe("Advanced LLM Settings", () => {
|
||||
it("should not render the runtime settings input if OSS mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
const input = screen.queryByTestId("runtime-settings-input");
|
||||
expect(input).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the runtime settings input if SaaS mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
screen.getByTestId("runtime-settings-input");
|
||||
});
|
||||
|
||||
it("should set the default runtime setting set", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
remote_runtime_resource_factor: 1,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(userEvent.setup());
|
||||
|
||||
const input = await screen.findByTestId("runtime-settings-input");
|
||||
expect(input).toHaveValue("1x (2 core, 8G)");
|
||||
});
|
||||
|
||||
it("should always have the runtime input disabled", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(userEvent.setup());
|
||||
|
||||
const input = await screen.findByTestId("runtime-settings-input");
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
|
||||
it.skip("should save the runtime settings when the 'Save Changes' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
const input = await screen.findByTestId("runtime-settings-input");
|
||||
await user.click(input);
|
||||
|
||||
const option = await screen.findByText("2x (4 core, 16G)");
|
||||
await user.click(option);
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
remote_runtime_resource_factor: 2,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("saving with no changes but having advanced enabled should hide the advanced items", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId("llm-custom-model-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("base-url-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("agent-input")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("enable-confirmation-mode-switch"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("resetting settings with no changes but having advanced enabled should hide the advanced items", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
const resetButton = screen.getByText("Reset to defaults");
|
||||
await user.click(resetButton);
|
||||
|
||||
// show modal
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
// confirm reset
|
||||
const confirmButton = within(modal).getByText("Reset");
|
||||
await user.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId("llm-custom-model-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("base-url-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("agent-input")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("enable-confirmation-mode-switch"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should save if only confirmation mode is enabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
await toggleAdvancedSettings(user);
|
||||
|
||||
const confirmationModeSwitch = await screen.findByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
await user.click(confirmationModeSwitch);
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
confirmation_mode: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should toggle advanced if user had set a custom model", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "some/custom-model",
|
||||
});
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
expect(advancedSwitch).toBeChecked();
|
||||
|
||||
const llmCustomInput = screen.getByTestId("llm-custom-model-input");
|
||||
expect(llmCustomInput).toBeInTheDocument();
|
||||
expect(llmCustomInput).toHaveValue("some/custom-model");
|
||||
});
|
||||
});
|
||||
|
||||
it("should have advanced settings enabled if the user previously had them enabled", async () => {
|
||||
const hasAdvancedSettingsSetSpy = vi.spyOn(
|
||||
AdvancedSettingsUtlls,
|
||||
"hasAdvancedSettingsSet",
|
||||
);
|
||||
hasAdvancedSettingsSetSpy.mockReturnValue(true);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
expect(advancedSwitch).toBeChecked();
|
||||
|
||||
const llmCustomInput = screen.getByTestId("llm-custom-model-input");
|
||||
expect(llmCustomInput).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should have confirmation mode enabled if the user previously had it enabled", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
confirmation_mode: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const confirmationModeSwitch = screen.getByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
expect(confirmationModeSwitch).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME: security analyzer is not found for some reason...
|
||||
it.skip("should have the values set if the user previously had them set", async () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
language: "no",
|
||||
github_token_is_set: true,
|
||||
user_consents_to_analytics: true,
|
||||
llm_base_url: "https://test.com",
|
||||
llm_model: "anthropic/claude-3-5-sonnet-20241022",
|
||||
agent: "CoActAgent",
|
||||
security_analyzer: "mock-invariant",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("language-input")).toHaveValue("Norsk");
|
||||
expect(screen.getByText("Disconnect from GitHub")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("enable-analytics-switch")).toBeChecked();
|
||||
expect(screen.getByTestId("advanced-settings-switch")).toBeChecked();
|
||||
expect(screen.getByTestId("base-url-input")).toHaveValue(
|
||||
"https://test.com",
|
||||
);
|
||||
expect(screen.getByTestId("llm-custom-model-input")).toHaveValue(
|
||||
"anthropic/claude-3-5-sonnet-20241022",
|
||||
);
|
||||
expect(screen.getByTestId("agent-input")).toHaveValue("CoActAgent");
|
||||
expect(
|
||||
screen.getByTestId("enable-confirmation-mode-switch"),
|
||||
).toBeChecked();
|
||||
expect(screen.getByTestId("security-analyzer-input")).toHaveValue(
|
||||
"mock-invariant",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should save the settings when the 'Save Changes' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const languageInput = await screen.findByTestId("language-input");
|
||||
await user.click(languageInput);
|
||||
|
||||
const norskOption = await screen.findByText("Norsk");
|
||||
await user.click(norskOption);
|
||||
|
||||
expect(languageInput).toHaveValue("Norsk");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_api_key: "", // empty because it's not set previously
|
||||
github_token: undefined,
|
||||
language: "no",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should properly save basic LLM model settings", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
// disable advanced mode
|
||||
const advancedSwitch = await screen.findByTestId(
|
||||
"advanced-settings-switch",
|
||||
);
|
||||
await user.click(advancedSwitch);
|
||||
|
||||
const providerInput = await screen.findByTestId("llm-provider-input");
|
||||
await user.click(providerInput);
|
||||
|
||||
const openaiOption = await screen.findByText("OpenAI");
|
||||
await user.click(openaiOption);
|
||||
|
||||
const modelInput = await screen.findByTestId("llm-model-input");
|
||||
await user.click(modelInput);
|
||||
|
||||
const gpt4Option = await screen.findByText("gpt-4o");
|
||||
await user.click(gpt4Option);
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
github_token: undefined,
|
||||
llm_api_key: "", // empty because it's not set previously
|
||||
llm_model: "openai/gpt-4o",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should reset the settings when the 'Reset to defaults' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const languageInput = await screen.findByTestId("language-input");
|
||||
await user.click(languageInput);
|
||||
|
||||
const norskOption = await screen.findByText("Norsk");
|
||||
await user.click(norskOption);
|
||||
|
||||
expect(languageInput).toHaveValue("Norsk");
|
||||
|
||||
const resetButton = screen.getByText("Reset to defaults");
|
||||
await user.click(resetButton);
|
||||
|
||||
expect(saveSettingsSpy).not.toHaveBeenCalled();
|
||||
|
||||
// show modal
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
// confirm reset
|
||||
const confirmButton = within(modal).getByText("Reset");
|
||||
await user.click(confirmButton);
|
||||
|
||||
const mockCopy: Partial<PostApiSettings> = {
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
};
|
||||
delete mockCopy.github_token_is_set;
|
||||
delete mockCopy.unset_github_token;
|
||||
delete mockCopy.user_consents_to_analytics;
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith({
|
||||
...mockCopy,
|
||||
github_token: undefined, // not set
|
||||
llm_api_key: "", // reset as well
|
||||
});
|
||||
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should cancel the reset when the 'Cancel' button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const resetButton = await screen.findByText("Reset to defaults");
|
||||
await user.click(resetButton);
|
||||
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
const cancelButton = within(modal).getByText("Cancel");
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(saveSettingsSpy).not.toHaveBeenCalled();
|
||||
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call handleCaptureConsent with true if the save is successful", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleCaptureConsentSpy = vi.spyOn(
|
||||
ConsentHandlers,
|
||||
"handleCaptureConsent",
|
||||
);
|
||||
renderSettingsScreen();
|
||||
|
||||
const analyticsConsentInput = await screen.findByTestId(
|
||||
"enable-analytics-switch",
|
||||
);
|
||||
|
||||
expect(analyticsConsentInput).not.toBeChecked();
|
||||
await user.click(analyticsConsentInput);
|
||||
expect(analyticsConsentInput).toBeChecked();
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should call handleCaptureConsent with false if the save is successful", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleCaptureConsentSpy = vi.spyOn(
|
||||
ConsentHandlers,
|
||||
"handleCaptureConsent",
|
||||
);
|
||||
renderSettingsScreen();
|
||||
|
||||
const saveButton = await screen.findByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("should not reset analytics consent when resetting to defaults", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
user_consents_to_analytics: true,
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const analyticsConsentInput = await screen.findByTestId(
|
||||
"enable-analytics-switch",
|
||||
);
|
||||
expect(analyticsConsentInput).toBeChecked();
|
||||
|
||||
const resetButton = await screen.findByText("Reset to defaults");
|
||||
await user.click(resetButton);
|
||||
|
||||
const modal = await screen.findByTestId("reset-modal");
|
||||
const confirmButton = within(modal).getByText("Reset");
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ user_consents_to_analytics: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("should render the security analyzer input if the confirmation mode is enabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
let securityAnalyzerInput = screen.queryByTestId(
|
||||
"security-analyzer-input",
|
||||
);
|
||||
expect(securityAnalyzerInput).not.toBeInTheDocument();
|
||||
|
||||
const confirmationModeSwitch = await screen.findByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
await user.click(confirmationModeSwitch);
|
||||
|
||||
securityAnalyzerInput = await screen.findByTestId(
|
||||
"security-analyzer-input",
|
||||
);
|
||||
expect(securityAnalyzerInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// FIXME: localStorage isn't being set
|
||||
it.skip("should save with ENABLE_DEFAULT_CONDENSER with true if user set the feature flag in local storage", async () => {
|
||||
localStorage.setItem("ENABLE_DEFAULT_CONDENSER", "true");
|
||||
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enable_default_condenser: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should send an empty LLM API Key if the user submits an empty string", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
const input = await screen.findByTestId("llm-api-key-input");
|
||||
expect(input).toHaveValue("");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ llm_api_key: "" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not send an empty LLM API Key if the user submits an empty string but already has it set", async () => {
|
||||
const user = userEvent.setup();
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_api_key: "**********",
|
||||
});
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
const input = await screen.findByTestId("llm-api-key-input");
|
||||
expect(input).toHaveValue("");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ llm_api_key: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("should submit the LLM API Key if it is the first time the user sets it", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsScreen();
|
||||
|
||||
const input = await screen.findByTestId("llm-api-key-input");
|
||||
await user.type(input, "new-api-key");
|
||||
|
||||
const saveButton = screen.getByText("Save Changes");
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ llm_api_key: "new-api-key" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { handleStatusMessage } from "#/services/actions";
|
||||
import store from "#/store";
|
||||
import { trackError } from "#/utils/error-handler";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("#/utils/error-handler", () => ({
|
||||
trackError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("#/store", () => ({
|
||||
default: {
|
||||
dispatch: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Actions Service", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("handleStatusMessage", () => {
|
||||
it("should dispatch info messages to status state", () => {
|
||||
const message = {
|
||||
type: "info",
|
||||
message: "Runtime is not available",
|
||||
id: "runtime.unavailable",
|
||||
status_update: true as const,
|
||||
};
|
||||
|
||||
handleStatusMessage(message);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: message,
|
||||
}));
|
||||
});
|
||||
|
||||
it("should log error messages and display them in chat", () => {
|
||||
const message = {
|
||||
type: "error",
|
||||
message: "Runtime connection failed",
|
||||
id: "runtime.connection.failed",
|
||||
status_update: true as const,
|
||||
};
|
||||
|
||||
handleStatusMessage(message);
|
||||
|
||||
expect(trackError).toHaveBeenCalledWith({
|
||||
message: "Runtime connection failed",
|
||||
source: "chat",
|
||||
metadata: { msgId: "runtime.connection.failed" },
|
||||
});
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(expect.objectContaining({
|
||||
payload: message,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { amountIsValid } from "#/utils/amount-is-valid";
|
||||
|
||||
describe("amountIsValid", () => {
|
||||
describe("fails", () => {
|
||||
test("when the amount is negative", () => {
|
||||
expect(amountIsValid("-5")).toBe(false);
|
||||
expect(amountIsValid("-25")).toBe(false);
|
||||
});
|
||||
|
||||
test("when the amount is zero", () => {
|
||||
expect(amountIsValid("0")).toBe(false);
|
||||
});
|
||||
|
||||
test("when an empty string is passed", () => {
|
||||
expect(amountIsValid("")).toBe(false);
|
||||
expect(amountIsValid(" ")).toBe(false);
|
||||
});
|
||||
|
||||
test("when a non-numeric value is passed", () => {
|
||||
expect(amountIsValid("abc")).toBe(false);
|
||||
expect(amountIsValid("1abc")).toBe(false);
|
||||
expect(amountIsValid("abc1")).toBe(false);
|
||||
});
|
||||
|
||||
test("when an amount less than the minimum is passed", () => {
|
||||
// test assumes the minimum is 10
|
||||
expect(amountIsValid("9")).toBe(false);
|
||||
expect(amountIsValid("9.99")).toBe(false);
|
||||
});
|
||||
|
||||
test("when an amount more than the maximum is passed", () => {
|
||||
// test assumes the minimum is 25000
|
||||
expect(amountIsValid("25001")).toBe(false);
|
||||
expect(amountIsValid("25000.01")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { trackError, showErrorToast, showChatError } from "#/utils/error-handler";
|
||||
import posthog from "posthog-js";
|
||||
import toast from "react-hot-toast";
|
||||
import * as Actions from "#/services/actions";
|
||||
|
||||
vi.mock("posthog-js", () => ({
|
||||
default: {
|
||||
captureException: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/services/actions", () => ({
|
||||
handleStatusMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Error Handler", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("trackError", () => {
|
||||
it("should send error to PostHog with basic info", () => {
|
||||
const error = {
|
||||
message: "Test error",
|
||||
source: "test",
|
||||
};
|
||||
|
||||
trackError(error);
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
|
||||
error_source: "test",
|
||||
});
|
||||
});
|
||||
|
||||
it("should include additional metadata in PostHog event", () => {
|
||||
const error = {
|
||||
message: "Test error",
|
||||
source: "test",
|
||||
metadata: {
|
||||
extra: "info",
|
||||
details: { foo: "bar" },
|
||||
},
|
||||
};
|
||||
|
||||
trackError(error);
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
|
||||
error_source: "test",
|
||||
extra: "info",
|
||||
details: { foo: "bar" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("showErrorToast", () => {
|
||||
it("should log error and show toast", () => {
|
||||
const error = {
|
||||
message: "Toast error",
|
||||
source: "toast-test",
|
||||
};
|
||||
|
||||
showErrorToast(error);
|
||||
|
||||
// Verify PostHog logging
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
|
||||
error_source: "toast-test",
|
||||
});
|
||||
|
||||
// Verify toast was shown
|
||||
expect(toast.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should include metadata in PostHog event when showing toast", () => {
|
||||
const error = {
|
||||
message: "Toast error",
|
||||
source: "toast-test",
|
||||
metadata: { context: "testing" },
|
||||
};
|
||||
|
||||
showErrorToast(error);
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
|
||||
error_source: "toast-test",
|
||||
context: "testing",
|
||||
});
|
||||
});
|
||||
|
||||
it("should log errors from different sources with appropriate metadata", () => {
|
||||
// Test agent status error
|
||||
showErrorToast({
|
||||
message: "Agent error",
|
||||
source: "agent-status",
|
||||
metadata: { id: "error.agent" },
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Agent error"), {
|
||||
error_source: "agent-status",
|
||||
id: "error.agent",
|
||||
});
|
||||
|
||||
showErrorToast({
|
||||
message: "Server error",
|
||||
source: "server",
|
||||
metadata: { error_code: 500, details: "Internal error" },
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Server error"), {
|
||||
error_source: "server",
|
||||
error_code: 500,
|
||||
details: "Internal error",
|
||||
});
|
||||
});
|
||||
|
||||
it("should log feedback submission errors with conversation context", () => {
|
||||
const error = new Error("Feedback submission failed");
|
||||
showErrorToast({
|
||||
message: error.message,
|
||||
source: "feedback",
|
||||
metadata: { conversationId: "123", error },
|
||||
});
|
||||
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Feedback submission failed"), {
|
||||
error_source: "feedback",
|
||||
conversationId: "123",
|
||||
error,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("showChatError", () => {
|
||||
it("should log error and show chat error message", () => {
|
||||
const error = {
|
||||
message: "Chat error",
|
||||
source: "chat-test",
|
||||
msgId: "123",
|
||||
};
|
||||
|
||||
showChatError(error);
|
||||
|
||||
// Verify PostHog logging
|
||||
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Chat error"), {
|
||||
error_source: "chat-test",
|
||||
});
|
||||
|
||||
// Verify error message was shown in chat
|
||||
expect(Actions.handleStatusMessage).toHaveBeenCalledWith({
|
||||
type: "error",
|
||||
message: "Chat error",
|
||||
id: "123",
|
||||
status_update: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it, test } from "vitest";
|
||||
import { hasAdvancedSettingsSet } from "#/utils/has-advanced-settings-set";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
describe("hasAdvancedSettingsSet", () => {
|
||||
it("should return false by default", () => {
|
||||
expect(hasAdvancedSettingsSet(DEFAULT_SETTINGS)).toBe(false);
|
||||
});
|
||||
|
||||
describe("should be true if", () => {
|
||||
test("LLM_BASE_URL is set", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
LLM_BASE_URL: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("AGENT is not default value", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
AGENT: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("REMOTE_RUNTIME_RESOURCE_FACTOR is not default value", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 999,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("CONFIRMATION_MODE is true", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
CONFIRMATION_MODE: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("SECURITY_ANALYZER is set", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
SECURITY_ANALYZER: "test",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isCustomModel } from "#/utils/is-custom-model";
|
||||
|
||||
describe("isCustomModel", () => {
|
||||
const models = ["anthropic/claude-3.5", "openai/gpt-3.5-turbo", "gpt-4o"];
|
||||
|
||||
it("should return false by default", () => {
|
||||
expect(isCustomModel(models, "")).toBe(false);
|
||||
});
|
||||
|
||||
it("should be true if it is a custom model", () => {
|
||||
expect(isCustomModel(models, "some/model")).toBe(true);
|
||||
});
|
||||
|
||||
it("should be false if it is not a custom model", () => {
|
||||
expect(isCustomModel(models, "anthropic/claude-3.5")).toBe(false);
|
||||
expect(isCustomModel(models, "openai/gpt-3.5-turbo")).toBe(false);
|
||||
expect(isCustomModel(models, "openai/gpt-4o")).toBe(false);
|
||||
});
|
||||
});
|
||||
Generated
+2298
-2279
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user