Compare commits

..

86 Commits

Author SHA1 Message Date
Engel Nyst
c8d856cfa5 tests(LLM): add concurrency and provider-mapping reinit tests; fix style
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 16:26:02 +00:00
Engel Nyst
db5c0c687c style(tests): apply pre-commit formatting fixes
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 05:27:36 +00:00
Engel Nyst
82d5b0388b tests(LLM): add reinit tests (basic, capability recompute, cost flag reset)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 05:24:38 +00:00
Engel Nyst
bf192688a5 LLM: add lock + reinit helper; recompute capabilities in centralized initializer; keep update_config alias
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 05:18:38 +00:00
Engel Nyst
477d9b17c0 Merge branch 'main' into llm-init to resolve conflicts
Co-authored-by: OpenHands-GPT-4.1 <openhands@all-hands.dev>
2025-08-14 03:31:27 +00:00
787627858
2f32064778 fix file_ handler to TimedRotatingFileHandler type to prevent log fil… (#10089)
Co-authored-by: liwei136 <liwei136@baidu.com>
2025-08-14 03:16:44 +00:00
Xingyao Wang
5e85986f32 docs: Update documentation to promote uv as recommended installation method (#10291)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-13 23:11:02 +00:00
Xingyao Wang
4f436922ca fix: browser title not updating when conversation title changes (#10275)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 05:07:59 +08:00
Xingyao Wang
d256348a46 refactor(git): principled way to set git configuration for agents & re-enable git settings in UI (#10293)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-13 20:45:15 +00:00
aeft
6bdc5563cf feat: allow partial modification of CLI settings (#10240) 2025-08-13 19:26:35 +00:00
Xingyao Wang
c2f46200c0 chore(lint): Apply comprehensive linting and formatting fixes (#10287)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-13 21:13:19 +02:00
Xingyao Wang
e39bf80239 fix(prompt): Add explicit GitHub/GitLab/Slack push instructions to templates (#10290)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-14 02:44:06 +08:00
Rohit Malhotra
368a0248e3 Modify experiment manager defaults for nested runtimes (#10269)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-13 14:41:28 -04:00
mamoodi
db9ceb380a Patch release 0.52.1 (#10284)
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
2025-08-13 14:16:34 -04:00
Copilot
c64971d0c4 Reorganize unit tests by source module into structured directory hierarchy (#10092)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: enyst <6080905+enyst@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-13 15:22:56 +00:00
llamantino
69fa580899 fix(misc): MCP settings and other UI improvements/fixes (#10141) 2025-08-13 10:30:38 -04:00
mamoodi
e3411f743d Release 0.52.0 (#10144) 2025-08-13 09:53:20 -04:00
Hiep Le
2b65b8aff2 fix(frontend): UI breaks when user message contains codeblock that's too wide (#10276) 2025-08-13 15:14:28 +04:00
Hiep Le
11f364c5e4 fix(frontend): UI does not display triggered microagent knowledge well. (#10277) 2025-08-13 14:31:05 +08:00
Jesse
4e3a862571 Add llm disable stop word env var (#10274)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-08-13 03:52:11 +00:00
Xingyao Wang
50aa014876 refactor(prompt): Consolidate system prompts with Jinja inheritance (#9797)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-13 03:04:08 +00:00
olyashok
500ab46918 Supprot for named volumes in docker_runtime (#10268) 2025-08-12 21:18:53 +00:00
Mislav Lukach
e311f3e70f fix(ui): increase settings page spacing (#10202) 2025-08-12 22:04:47 +04:00
Mislav Lukach
f68ad3695c Feat/maintenance banner dismissible (#10072)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-08-12 22:02:36 +04:00
mamoodi
ed711318e4 Hide git settings again (#10261) 2025-08-12 17:30:57 +00:00
Calvin Smith
57a3d8f17d fix: Solvability setting not stored (#10258)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-08-12 12:54:45 -04:00
jpelletier1
e1559651b8 Unhide Git Settings feature and add explanatory text (#10256)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-12 14:18:15 +00:00
Ibragim Badertdinov
19a6b6b618 feat(eval): Support evaluation on SWE-rebench (#10251) 2025-08-12 14:05:43 +00:00
Xingyao Wang
2b7e44819f chore(agent_prompt): Add EXTERNAL_SERVICES section to system prompt template (#10244)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-12 21:53:53 +08:00
Xingyao Wang
0699a0ce7c fix: copy microagents file into runtime image (#10245)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-08-12 12:42:42 +00:00
Insop
1d0d88d491 Readability improvement & remove duplicated and unused prompts (#10241) 2025-08-12 12:42:17 +08:00
Tim O'Farrell
6f21b6700a Fix for issues where callbacks are not batched (#10235) 2025-08-11 15:44:48 -06:00
Tim O'Farrell
af49b615b1 Add BatchedWebHookFileStore for batching webhook updates (#10119)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-11 12:51:08 -06:00
Tim O'Farrell
4651edd5b3 Fix circular import by moving refine_prompt to dedicated module (#10223)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-11 12:17:18 -06:00
olyashok
d7f72fec9c OverlayFS support for docker runtimes (#10222) 2025-08-11 18:11:08 +00:00
mamoodi
09011c91f8 Remove rbren from UI changes reviewers (#10230) 2025-08-11 13:32:29 -04:00
Xingyao Wang
e56fabfc5e feat(cli): Add markdown schema visualization in CLI (#10193)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-11 15:47:38 +00:00
Xingyao Wang
56f752557c Implement auto-pagination for conversation list with infinite scroll (#10129)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-08-11 15:03:29 +00:00
Calvin Smith
5f2ad7fbb0 Solvability setting switch (#9727)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-08-11 08:57:47 -06:00
Ryan H. Tran
758e30c9a8 Remove SecretStr conversion in GAIA eval (#10204) 2025-08-11 21:30:18 +08:00
dependabot[bot]
28017f232e chore(deps): bump the version-all group across 1 directory with 9 updates (#10168)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 14:51:36 +04:00
Tim O'Farrell
3302c31c60 Removed Hack that is no longer required (#10195) 2025-08-10 12:13:19 -06:00
Xingyao Wang
116ba199d1 feat(agent): stop using short tool description for gpt-5 (#10184) 2025-08-09 17:56:52 -04:00
Boxuan Li
803bdced9c Fix Windows prompt refinement: ensure 'bash' is replaced with 'powershell' in all prompts (#10179)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 20:28:36 -07:00
Xingyao Wang
3eecac2003 docs: Add GPT-5 model recommendation and fix pricing display issue (#10177)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 19:19:59 +00:00
mamoodi
c02e09fc2d Hide Git Settings section from Application settings (#10176)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 19:06:40 +00:00
Tim O'Farrell
18f8661770 feat: add mcp_shttp_servers override to conversation initialization (#10171)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 18:05:44 +00:00
Xingyao Wang
04ff4a025b feat(cli): Use CLI to launch OpenHands UI server via Docker (#9783)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-09 02:04:07 +08:00
mamoodi
81ef363658 Increase stale bot inactivity time and better messaging (#10167)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-08 16:41:15 +00:00
Xingyao Wang
1474c5bc1c Support gpt-5-2025-08-07 and add it to OpenHands provider (#10172)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 16:05:51 +00:00
sp.wack
9b0a5da839 Use EventStore directly in remember prompt; merge client services (#10143)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 18:03:03 +04:00
Graham Neubig
7ab2ad2c1b Fix authentication setup issues in unit tests (#10118)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-07 22:12:21 -04:00
Graham Neubig
8416a019cb Fix unit test failures by prioritizing current directory in PYTHONPATH (#10105)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-07 22:12:02 -04:00
Engel Nyst
73a7c7786d Load previous conversation by id (CLI) (#10156) 2025-08-07 23:09:20 +02:00
aeft
11d12c5a01 fix: prevent CLI argument parser defaults from overriding config file values (#10140) 2025-08-08 04:48:04 +08:00
Xingyao Wang
c4f303a07b chore(eval): Remove eval_infer_remote.sh script and related references (#10157)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-07 20:46:59 +00:00
Kenny Dizi
3a629cdf08 Add support model claude-opus-4-1-20250805 (#10120) 2025-08-07 18:48:34 +00:00
sp.wack
6ea33b657d chore(frontend): Remove some dead code (#10121) 2025-08-08 02:40:35 +08:00
Xingyao Wang
a526f53181 Add uvx CLI command to PR descriptions (#10142)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 01:51:55 +08:00
Xingyao Wang
0d28113df1 Fix Docker installation for swebench and mswebench images (#10124)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-07 23:42:35 +08:00
aeft
029a19ca05 fix: remove duplicate error message in provider validator (#10088) 2025-08-07 23:37:51 +08:00
Xingyao Wang
d525c5ad93 fix(config): support defining MCP servers via environment variables and improve logging (#10069)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-07 14:48:44 +00:00
chuckbutkus
881729b49c Fix user info api calls (#10137)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-06 23:57:52 -04:00
sp.wack
42ed36e5cc hotfix(frontend): Fix chat message font size (#10134) 2025-08-06 18:37:06 +00:00
Xingyao Wang
2b4e9137e3 chore(logging): Reduce microagents directory logging noise from WARNING to DEBUG (#10127)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-06 20:26:42 +02:00
greese-insight
37cebc1f8f fix: update git config to handle the necessary user name and email se… (#9975) 2025-08-06 20:25:26 +02:00
Graham Neubig
59ecf5515e Promote OpenHands LLM provider as the recommended option (#10108)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-06 13:33:12 -04:00
Rohit Malhotra
3f327a940f Paginate repo list from providers (#9826)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
2025-08-06 13:03:46 -04:00
mamoodi
9c83a5623f Remove the "No secrets found" which is unnecessary (#10126)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-06 12:55:32 -04:00
Xingyao Wang
efa3c2187d Bump conversation history limit from 20 to 100 (#10128)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-06 16:43:31 +00:00
Jamesz12b
12bc965964 fix: Chat Width Limitation in Chat Window (#9895) 2025-08-06 16:11:56 +00:00
dependabot[bot]
256bad9f5a chore(deps-dev): bump eslint-plugin-prettier from 5.5.3 to 5.5.4 in /frontend in the eslint group (#10123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 15:26:19 +00:00
Tim O'Farrell
e9700ecc3d Add "Session Timeout!" translation entry (#10122)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-06 15:00:01 +00:00
Graham Neubig
eba4294b08 Add Git credentials settings to frontend (#9956)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Abubakar <abubakaran102025@gmail.com>
2025-08-06 09:54:19 -04:00
Hiep Le
dbba60356e chore: remove the feature flag for the microagent management page. (#9874) 2025-08-06 17:46:05 +04:00
Hiep Le
dceff1fae4 feat(frontend): add a tooltip to repo dropdown on home page (#10079) 2025-08-06 17:16:18 +04:00
dependabot[bot]
5a35fa571a chore(deps): bump the version-all group in /frontend with 5 updates (#10084)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 17:12:55 +04:00
chuckbutkus
ff2cfb7bce Get auth URL from config if it is supplied. (#10111) 2025-08-06 08:58:08 -04:00
Graham Neubig
1c66347803 Improve stop button message for better user experience (#9860)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-05 21:53:40 -04:00
OpenHands Bot
6756427217 🤖 Auto-fix Python linting issues 2025-07-30 23:33:14 +00:00
Engel Nyst
1b74920509 Merge branch 'main' into llm-init 2025-07-31 01:31:31 +02:00
Engel Nyst
c933b88f36 Merge branch 'main' into llm-init 2025-07-30 02:01:52 +02:00
Engel Nyst
e0e9d3d07c Merge branch 'main' into llm-init 2025-07-26 22:50:17 +02:00
Engel Nyst
00f9ff08c7 Merge branch 'main' into llm-init 2025-07-22 00:53:46 +02:00
Engel Nyst
f37f8fb723 Merge branch 'main' into llm-init 2025-07-21 01:57:07 +02:00
Engel Nyst
8628da0037 Refactor LLM class to support runtime configuration updates
- Decouple partial function creation from LLM initialization
- Extract _build_completion_function() method for reusable completion setup
- Add update_config() method for hot-swapping LLM configuration at runtime
- Add _rebuild_completion_wrapper() method to recreate retry decorator wrapper
- Preserve metrics and retry listener instances during config updates
- Handle model changes by resetting model info for re-initialization
- Support custom tokenizer updates and log completion folder creation
- Add comprehensive unit tests covering all update scenarios

This prepares the LLM class for the upcoming unified configuration system
that will enable runtime config reloading without restart.

Co-authored-by: OpenHands <openhands@all-hands.dev>
2025-07-19 15:07:52 +02:00
368 changed files with 9749 additions and 4028 deletions

2
.github/CODEOWNERS vendored
View File

@@ -2,7 +2,7 @@
# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Frontend code owners
/frontend/ @rbren @amanape
/frontend/ @amanape
/openhands-ui/ @amanape
# Evaluation code owners

71
.github/scripts/update_pr_description.sh vendored Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/bash
set -euxo pipefail
# This script updates the PR description with commands to run the PR locally
# It adds both Docker and uvx commands
# Get the branch name for the PR
BRANCH_NAME=$(gh pr view "$PR_NUMBER" --json headRefName --jq .headRefName)
# Define the Docker command
DOCKER_RUN_COMMAND="docker run -it --rm \
-p 3000:3000 \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:${SHORT_SHA}-nikolaik \
--name openhands-app-${SHORT_SHA} \
docker.all-hands.dev/all-hands-ai/openhands:${SHORT_SHA}"
# Define the uvx command
UVX_RUN_COMMAND="uvx --python 3.12 --from git+https://github.com/All-Hands-AI/OpenHands@${BRANCH_NAME} openhands"
# Get the current PR body
PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq .body)
# Prepare the new PR body with both commands
if echo "$PR_BODY" | grep -q "To run this PR locally, use the following command:"; then
# For existing PR descriptions, use a more robust approach
# Split the PR body at the "To run this PR locally" section and replace everything after it
BEFORE_SECTION=$(echo "$PR_BODY" | sed '/To run this PR locally, use the following command:/,$d')
NEW_PR_BODY=$(cat <<EOF
${BEFORE_SECTION}
To run this PR locally, use the following command:
GUI with Docker:
\`\`\`
${DOCKER_RUN_COMMAND}
\`\`\`
CLI with uvx:
\`\`\`
${UVX_RUN_COMMAND}
\`\`\`
EOF
)
else
# For new PR descriptions: use heredoc safely without indentation
NEW_PR_BODY=$(cat <<EOF
$PR_BODY
---
To run this PR locally, use the following command:
GUI with Docker:
\`\`\`
${DOCKER_RUN_COMMAND}
\`\`\`
CLI with uvx:
\`\`\`
${UVX_RUN_COMMAND}
\`\`\`
EOF
)
fi
# Update the PR description
echo "Updating PR description with Docker and uvx commands"
gh pr edit "$PR_NUMBER" --body "$NEW_PR_BODY"

View File

@@ -332,29 +332,5 @@ jobs:
SHORT_SHA: ${{ steps.short_sha.outputs.SHORT_SHA }}
shell: bash
run: |
echo "updating PR description"
DOCKER_RUN_COMMAND="docker run -it --rm \
-p 3000:3000 \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:$SHORT_SHA-nikolaik \
--name openhands-app-$SHORT_SHA \
docker.all-hands.dev/all-hands-ai/openhands:$SHORT_SHA"
PR_BODY=$(gh pr view $PR_NUMBER --json body --jq .body)
if echo "$PR_BODY" | grep -q "To run this PR locally, use the following command:"; then
UPDATED_PR_BODY=$(echo "${PR_BODY}" | sed -E "s|docker run -it --rm.*|$DOCKER_RUN_COMMAND|")
else
UPDATED_PR_BODY="${PR_BODY}
---
To run this PR locally, use the following command:
\`\`\`
$DOCKER_RUN_COMMAND
\`\`\`"
fi
echo "updated body: $UPDATED_PR_BODY"
gh pr edit $PR_NUMBER --body "$UPDATED_PR_BODY"
echo "Updating PR description with Docker and uvx commands"
bash ${GITHUB_WORKSPACE}/.github/scripts/update_pr_description.sh

View File

@@ -48,11 +48,11 @@ jobs:
- name: Build Environment
run: make build
- name: Run Unit Tests
run: poetry run pytest --forked -n auto -svv ./tests/unit
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv ./tests/unit
- name: Run Runtime Tests with CLIRuntime
run: TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py
- name: Run E2E Tests
run: poetry run pytest -svv tests/e2e
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest -svv tests/e2e
# Run specific Windows python tests
test-on-windows:
@@ -77,9 +77,11 @@ jobs:
- name: Run Windows unit tests
run: poetry run pytest -svv tests/unit/test_windows_bash.py
env:
PYTHONPATH: ".;$env:PYTHONPATH"
DEBUG: "1"
- name: Run Windows runtime tests with LocalRuntime
run: $env:TEST_RUNTIME="local"; poetry run pytest -svv tests/runtime/test_bash.py
env:
PYTHONPATH: ".;$env:PYTHONPATH"
TEST_RUNTIME: local
DEBUG: "1"

View File

@@ -12,11 +12,11 @@ jobs:
steps:
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
days-before-stale: 30
stale-issue-message: 'This issue is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'
stale-pr-message: 'This PR is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'
days-before-stale: 40
exempt-issue-labels: 'roadmap'
close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
days-before-close: 7
close-issue-message: 'This issue was automatically closed due to 50 days of inactivity. We do this to help keep the issues somewhat manageable and focus on active issues.'
close-pr-message: 'This PR was closed because it had no activity for 50 days. If you feel this was closed in error, and you would like to continue the PR, please resubmit or let us know.'
days-before-close: 10
operations-per-run: 150

View File

@@ -159,7 +159,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.51-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.52-nikolaik`
## Develop inside Docker container

View File

@@ -52,37 +52,63 @@ which comes with $20 in free credits for new users.
## 💻 Running OpenHands Locally
OpenHands can also run on your local system using Docker.
See the [Running OpenHands](https://docs.all-hands.dev/usage/installation) guide for
system requirements and more information.
### Option 1: CLI Launcher (Recommended)
> [!WARNING]
> On a public network? See our [Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)
> to secure your deployment by restricting network binding and implementing additional security measures.
The easiest way to run OpenHands locally is using the CLI launcher with [uv](https://docs.astral.sh/uv/). This provides better isolation from your current project's virtual environment and is required for OpenHands' default MCP servers.
**Install uv** (if you haven't already):
See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for the latest installation instructions for your platform.
**Launch OpenHands**:
```bash
# Launch the GUI server
uvx --python 3.12 --from openhands-ai openhands serve
# Or launch the CLI
uvx --python 3.12 --from openhands-ai openhands
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000) (for GUI mode)!
### Option 2: Docker
<details>
<summary>Click to expand Docker command</summary>
You can also run OpenHands directly with Docker:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.51
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
</details>
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
> [!WARNING]
> On a public network? See our [Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)
> to secure your deployment by restricting network binding and implementing additional security measures.
### Getting Started
When you open the application, you'll be asked to choose an LLM provider and add an API key.
[Anthropic's Claude Sonnet 4](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-20250514`)
works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
See the [Running OpenHands](https://docs.all-hands.dev/usage/installation) guide for
system requirements and more information.
## 💡 Other ways to run OpenHands
> [!WARNING]
@@ -93,8 +119,8 @@ works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
> [OpenHands Cloud Helm Chart](https://github.com/all-Hands-AI/OpenHands-cloud)
You can [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/usage/how-to/headless-mode),
interact with it via a [friendly CLI](https://docs.all-hands.dev/usage/how-to/cli-mode),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/usage/how-to/headless-mode),
or run it on tagged issues with [a github action](https://docs.all-hands.dev/usage/how-to/github-action).
Visit [Running OpenHands](https://docs.all-hands.dev/usage/installation) for more information and setup instructions.

View File

@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.51
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
> **注意**: 如果您在0.44版本之前使用过OpenHands您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。

View File

@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.51
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。

View File

@@ -93,8 +93,7 @@ def build_vscode_extension():
def build(setup_kwargs):
"""
This function is called by Poetry during the build process.
"""This function is called by Poetry during the build process.
`setup_kwargs` is a dictionary that will be passed to `setuptools.setup()`.
"""
print('--- Running custom Poetry build script (build_vscode.py) ---')

View File

@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.51-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.52-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -40,7 +40,7 @@ repos:
hooks:
- id: mypy
additional_dependencies:
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, pydantic, lxml]
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, types-Markdown, pydantic, lxml]
# To see gaps add `--html-report mypy-report/`
entry: mypy --config-file dev_config/python/mypy.ini openhands/
always_run: true

View File

@@ -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.51-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.52-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 for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -71,6 +71,7 @@
{
"group": "Providers",
"pages": [
"usage/llms/openhands-llms",
"usage/llms/azure-llms",
"usage/llms/google-llms",
"usage/llms/groq",
@@ -78,7 +79,6 @@
"usage/llms/litellm-proxy",
"usage/llms/moonshot",
"usage/llms/openai-llms",
"usage/llms/openhands-llms",
"usage/llms/openrouter"
]
}

View File

@@ -20,27 +20,42 @@ for scripting.
### Running with Python
**Note** - OpenHands requires Python version 3.12 or higher (Python 3.14 is not currently supported) and `uvx` for the default `fetch` MCP server (more details below).
**Note** - OpenHands requires Python version 3.12 or higher (Python 3.14 is not currently supported) and `uv` for the default `fetch` MCP server (more details below).
1. Install OpenHands using pip:
```bash
pip install openhands-ai
```
#### Recommended: Using uv
Or if you prefer not to manage your own Python environment, you can use `uvx`:
We recommend using [uv](https://docs.astral.sh/uv/) for the best OpenHands experience. uv provides better isolation from your current project's virtual environment and is required for OpenHands' default MCP servers.
1. **Install uv** (if you haven't already):
See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for the latest installation instructions for your platform.
2. **Launch OpenHands CLI**:
```bash
uvx --python 3.12 --from openhands-ai openhands
```
<AccordionGroup>
<Accordion title="Alternative: Traditional pip installation">
If you prefer to use pip:
```bash
# Install OpenHands
pip install openhands-ai
```
Note that you'll still need `uv` installed for the default MCP servers to work properly.
</Accordion>
<Accordion title="Create shell aliases for easy access across environments">
Add the following to your shell configuration file (`.bashrc`, `.zshrc`, etc.):
```bash
# Add OpenHands aliases
# Add OpenHands aliases (recommended)
alias openhands="uvx --python 3.12 --from openhands-ai openhands"
alias oh="uvx --python 3.12 --from openhands-ai openhands"
```
@@ -72,18 +87,19 @@ source ~/.bashrc # or source ~/.zshrc
</AccordionGroup>
2. Launch an interactive OpenHands conversation from the command line:
3. Launch an interactive OpenHands conversation from the command line:
```bash
openhands
# If using uvx (recommended)
uvx --python 3.12 --from openhands-ai openhands
```
<Note>
If you have cloned the repository, you can also run the CLI directly using Poetry:
poetry run python -m openhands.cli.main
poetry run openhands
</Note>
3. Set your model, API key, and other preferences using the UI (or alternatively environment variables, below).
4. Set your model, API key, and other preferences using the UI (or alternatively environment variables, below).
This command opens an interactive prompt where you can type tasks or commands and get responses from OpenHands.
The first time you run the CLI, it will take you through configuring the required LLM
@@ -103,7 +119,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -112,7 +128,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--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.51 \
docker.all-hands.dev/all-hands-ai/openhands:0.52 \
python -m openhands.cli.main --override-cli-mode true
```

View File

@@ -7,6 +7,67 @@ description: High level overview of the Graphical User Interface (GUI) in OpenHa
- [OpenHands is running](/usage/local-setup)
## Launching the GUI Server
### Using the CLI Command
You can launch the OpenHands GUI server directly from the command line using the `serve` command:
<Callout type="info">
**Prerequisites**: You need to have the [OpenHands CLI installed](/usage/how-to/cli-mode) first, OR have `uv` installed and run `uvx --python 3.12 --from openhands-ai openhands serve`. Otherwise, you'll need to use Docker directly (see the [Docker section](#using-docker-directly) below).
</Callout>
```bash
openhands serve
```
This command will:
- Check that Docker is installed and running
- Pull the required Docker images
- Launch the OpenHands GUI server at http://localhost:3000
- Use the same configuration directory (`~/.openhands`) as the CLI mode
#### Mounting Your Current Directory
To mount your current working directory into the GUI server container, use the `--mount-cwd` flag:
```bash
openhands serve --mount-cwd
```
This is useful when you want to work on files in your current directory through the GUI. The directory will be mounted at `/workspace` inside the container.
#### Using GPU Support
If you have NVIDIA GPUs and want to make them available to the OpenHands container, use the `--gpu` flag:
```bash
openhands serve --gpu
```
This will enable GPU support via nvidia-docker, mounting all available GPUs into the container. You can combine this with other flags:
```bash
openhands serve --gpu --mount-cwd
```
**Prerequisites for GPU support:**
- NVIDIA GPU drivers must be installed on your host system
- [NVIDIA Container Toolkit (nvidia-docker2)](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) must be installed and configured
#### Requirements
Before using the `openhands serve` command, ensure that:
- Docker is installed and running on your system
- You have internet access to pull the required Docker images
- Port 3000 is available on your system
The CLI will automatically check these requirements and provide helpful error messages if anything is missing.
### Using Docker Directly
Alternatively, you can run the GUI server using Docker directly. See the [local setup guide](/usage/local-setup) for detailed Docker instructions.
## Overview
### Initial Setup

View File

@@ -61,7 +61,7 @@ export GITHUB_TOKEN="your-token" # Required for repository operations
# Run OpenHands
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -73,7 +73,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--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.51 \
docker.all-hands.dev/all-hands-ai/openhands:0.52 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -18,7 +18,7 @@ Based on these findings and community feedback, these are the latest models that
### Cloud / API-Based Models
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
- [openai/o4-mini](https://openai.com/index/introducing-o3-and-o4-mini/)
- [openai/gpt-5-2025-08-07](https://openai.com/api/) (recommended)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [moonshot/kimi-k2-0711-preview](https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2)

View File

@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.51
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
2. Wait until the server is running (see log below):
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.51
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.52
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None

View File

@@ -32,4 +32,4 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
Pricing follows official API provider rates. [You can view model prices here.](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json)
For `qwen3-coder-480b`, we charge the cheapest FP8 rate available on openrouter: $0.4 per million input tokens and $1.6 per million output tokens.
For `qwen3-coder-480b`, we charge the cheapest FP8 rate available on openrouter: \$0.4 per million input tokens and \$1.6 per million output tokens.

View File

@@ -66,20 +66,64 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
### Start the App
#### Option 1: Using the CLI Launcher with uv (Recommended)
We recommend using [uv](https://docs.astral.sh/uv/) for the best OpenHands experience. uv provides better isolation from your current project's virtual environment and is required for OpenHands' default MCP servers (like the [fetch MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/fetch)).
**Install uv** (if you haven't already):
See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for the latest installation instructions for your platform.
**Launch OpenHands**:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
# Launch the GUI server
uvx --python 3.12 --from openhands-ai openhands serve
# Or with GPU support (requires nvidia-docker)
uvx --python 3.12 --from openhands-ai openhands serve --gpu
# Or with current directory mounted
uvx --python 3.12 --from openhands-ai openhands serve --mount-cwd
```
This will automatically handle Docker requirements checking, image pulling, and launching the GUI server. The `--gpu` flag enables GPU support via nvidia-docker, and `--mount-cwd` mounts your current directory into the container.
<Accordion title="Alternative: Traditional pip installation">
If you prefer to use pip and have Python 3.12+ installed:
```bash
# Install OpenHands
pip install openhands-ai
# Launch the GUI server
openhands serve
```
Note that you'll still need `uv` installed for the default MCP servers to work properly.
</Accordion>
#### Option 2: Using Docker Directly
<Accordion title="Docker Command (Click to expand)">
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.52-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.51
docker.all-hands.dev/all-hands-ai/openhands:0.52
```
</Accordion>
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
You'll find OpenHands running at http://localhost:3000!
@@ -100,6 +144,16 @@ OpenHands requires an API key to access most language models. Here's how to get
<AccordionGroup>
<Accordion title="OpenHands (Recommended)">
1. [Log in to OpenHands Cloud](https://app.all-hands.dev).
2. Go to the Settings page and navigate to the `API Keys` tab.
3. Copy your `LLM API Key`.
OpenHands provides access to state-of-the-art agentic coding models with competitive pricing. [Learn more about OpenHands LLM provider](/usage/llms/openhands-llms).
</Accordion>
<Accordion title="Anthropic (Claude)">
1. [Create an Anthropic account](https://console.anthropic.com/).

View File

@@ -18,8 +18,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -172,7 +172,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--answerer_model', '-a', default='gpt-3.5-turbo', help='answerer model'
)

View File

@@ -26,8 +26,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -506,7 +506,6 @@ def commit0_setup(dataset: pd.DataFrame, repo_split: str) -> pd.DataFrame:
Returns:
Filtered dataset based on split type
"""
filtered_dataset = pd.concat(
[
dataset[dataset['repo'].str.split('/').str[1] == repo]
@@ -525,7 +524,7 @@ def commit0_setup(dataset: pd.DataFrame, repo_split: str) -> pd.DataFrame:
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,

View File

@@ -89,8 +89,7 @@ def get_config(
def get_dv_query_for_real(
datasets, question, domain_knowledge=None, workflow_tags=None
):
"""
Prepare a structured query for the agent to execute on the specified datasets.
"""Prepare a structured query for the agent to execute on the specified datasets.
This function constructs a query by compiling metadata from the provided datasets, along with any relevant domain knowledge and workflow tags.
@@ -104,7 +103,6 @@ def get_dv_query_for_real(
query_to_dv: Query to be run on the dataset
dataset_meta: Metadata of the dataset
"""
dataset_meta = ''
for dataset_metadata in datasets:
dataset_meta += 'Dataset name: ' + dataset_metadata['name']
@@ -140,8 +138,7 @@ def get_dv_query_for_real(
def initialize_runtime(runtime: Runtime, data_files: list[str]):
"""
Initialize the runtime for the agent.
"""Initialize the runtime for the agent.
This function is called before the runtime is used to run the agent.
"""
@@ -231,8 +228,7 @@ def process_instance(
metadata: EvalMetadata,
reset_logger: bool = True,
):
"""
Process and evaluate a single instance of the dataset.
"""Process and evaluate a single instance of the dataset.
This function executes the OpenHands agent
for a specific instance of the dataset. It retrieves
@@ -247,7 +243,6 @@ def process_instance(
Returns:
output: EvalOutput object
"""
config = get_config(metadata)
# Setup the logger properly, so you can run
@@ -356,8 +351,7 @@ def list_csv_files(list_of_datasets):
def create_dataset(repo_location: str, split: str = 'test'):
"""
Create a dataset from the discoverybench repository
"""Create a dataset from the discoverybench repository
by walking through the repository and extracting metadata
from the metadata_{}.json files
@@ -368,7 +362,6 @@ def create_dataset(repo_location: str, split: str = 'test'):
Returns:
df: DataFrame containing the dataset instances
"""
data_dict = {}
data_location = os.path.join(repo_location, 'discoverybench', 'real', split)

View File

@@ -10,7 +10,6 @@ import huggingface_hub
import pandas as pd
from datasets import load_dataset
from PIL import Image
from pydantic import SecretStr
from evaluation.benchmarks.gaia.scorer import question_scorer
from evaluation.benchmarks.gaia.utils import (
@@ -31,8 +30,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
load_from_toml,
)
from openhands.core.config.utils import get_agent_config_arg
@@ -80,8 +79,7 @@ def get_config(
config_copy = copy.deepcopy(config)
load_from_toml(config_copy)
if config_copy.search_api_key:
config.search_api_key = SecretStr(config_copy.search_api_key)
config.search_api_key = config_copy.search_api_key
return config
@@ -294,7 +292,7 @@ Here is the task:
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--level',
type=str,

View File

@@ -20,8 +20,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -134,7 +134,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--hubs',
type=str,

View File

@@ -38,8 +38,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -312,7 +312,7 @@ Ok now its time to start solving the question. Good luck!
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
# data split must be one of 'gpqa_main', 'gqpa_diamond', 'gpqa_experts', 'gpqa_extended'
parser.add_argument(
'--data-split',

View File

@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
from openhands.core.config import (
LLMConfig,
OpenHandsConfig,
get_parser,
get_evaluation_parser,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
@@ -167,7 +167,7 @@ def process_predictions(predictions_path: str):
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'-s',
'--eval-split',

View File

@@ -30,8 +30,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
@@ -358,7 +358,7 @@ Be thorough in your exploration, testing, and reasoning. It's fine if your think
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'-s',
'--eval-split',

View File

@@ -18,8 +18,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -267,7 +267,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,

View File

@@ -23,8 +23,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -229,7 +229,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
SUBSETS = [
# Eurus subset: https://arxiv.org/abs/2404.02078

View File

@@ -4,7 +4,11 @@ import pprint
import tqdm
from openhands.core.config import get_llm_config_arg, get_parser, load_openhands_config
from openhands.core.config import (
get_evaluation_parser,
get_llm_config_arg,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM
@@ -111,7 +115,7 @@ def classify_error(llm: LLM, failed_case: dict) -> str:
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--json_file_path',
type=str,

View File

@@ -34,8 +34,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
@@ -273,7 +273,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'-s',
'--eval-split',

View File

@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.core.config import (
LLMConfig,
OpenHandsConfig,
get_parser,
get_evaluation_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
@@ -105,8 +105,7 @@ def process_instance(
log_dir: str | None = None,
runtime_failure_count: int = 0,
) -> EvalOutput:
"""
Evaluate agent performance on a SWE-bench problem instance.
"""Evaluate agent performance on a SWE-bench problem instance.
Note that this signature differs from the expected input to `run_evaluation`. Use
`functools.partial` to provide optional arguments before passing to the evaluation harness.
@@ -323,7 +322,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--input-file',
type=str,

View File

@@ -32,8 +32,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -772,7 +772,7 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
if __name__ == '__main__':
# pdb.set_trace()
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,

View File

@@ -21,8 +21,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -239,7 +239,7 @@ If the program uses some packages that are incompatible, please figure out alter
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--use-knowledge',
type=str,

View File

@@ -2,6 +2,8 @@
This folder contains the evaluation harness that we built on top of the original [SWE-Bench benchmark](https://www.swebench.com/) ([paper](https://arxiv.org/abs/2310.06770)).
**UPDATE (8/12/2025): We now support running SWE-rebench evaluation (see the paper [here](https://arxiv.org/abs/2505.20411))! For how to run it, checkout [this README](./SWE-rebench.md).**
**UPDATE (6/15/2025): We now support running SWE-bench-Live evaluation (see the paper [here](https://arxiv.org/abs/2505.23419))! For how to run it, checkout [this README](./SWE-bench-Live.md).**
**UPDATE (5/26/2025): We now support running interactive SWE-Bench evaluation (see the paper [here](https://arxiv.org/abs/2502.13069))! For how to run it, checkout [this README](./SWE-Interact.md).**
@@ -183,24 +185,7 @@ The final results will be saved to `evaluation/evaluation_outputs/outputs/swe_be
- `report.json`: a JSON file that contains keys like `"resolved_ids"` pointing to instance IDs that are resolved by the agent.
- `logs/`: a directory of test logs
### Run evaluation with `RemoteRuntime`
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to run rollout in parallel in the cloud, so you don't need a powerful machine to run evaluation.
Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
```bash
./evaluation/benchmarks/swe_bench/scripts/eval_infer_remote.sh [output.jsonl filepath] [num_workers]
# Example - This evaluates patches generated by CodeActAgent on Llama-3.1-70B-Instruct-Turbo on "princeton-nlp/SWE-bench_Lite"'s test set, with 16 number of workers running in parallel
ALLHANDS_API_KEY="YOUR-API-KEY" RUNTIME=remote SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev" EVAL_DOCKER_IMAGE_PREFIX="us-central1-docker.pkg.dev/evaluation-092424/swe-bench-images" \
evaluation/benchmarks/swe_bench/scripts/eval_infer_remote.sh evaluation/evaluation_outputs/outputs/swe-bench-lite/CodeActAgent/Llama-3.1-70B-Instruct-Turbo_maxiter_100_N_v1.9-no-hint/output.jsonl 16 "princeton-nlp/SWE-bench_Lite" "test"
```
To clean-up all existing runtimes that you've already started, run:
```bash
ALLHANDS_API_KEY="YOUR-API-KEY" ./evaluation/utils/scripts/cleanup_remote_runtime.sh
```
## SWT-Bench Evaluation

View File

@@ -0,0 +1,84 @@
# SWE-rebench
<p align="center">
<a href="https://arxiv.org/abs/2505.20411">📃 Paper</a>
<a href="https://huggingface.co/datasets/nebius/SWE-rebench">🤗 HuggingFace</a>
<a href="https://swe-rebench.com/leaderboard">📊 Leaderboard</a>
</p>
SWE-rebench is a large-scale dataset for verifiable software engineering tasks.
It comes in **two datasets**:
* **[`nebius/SWE-rebench-leaderboard`](https://huggingface.co/datasets/nebius/SWE-rebench-leaderboard)** updatable benchmark used for [leaderboard evaluation](https://swe-rebench.com/leaderboard).
* **[`nebius/SWE-rebench`](https://huggingface.co/datasets/nebius/SWE-rebench)** full dataset with **21,302 tasks**, suitable for training or large-scale offline evaluation.
This document explains how to run OpenHands on SWE-rebench, using the leaderboard split as the main example.
To run on the full dataset, simply replace the dataset name.
## Setting Up
Set up your development environment and configure your LLM provider by following the [SWE-bench README](README.md) in this directory.
## Running Inference
Use the existing SWE-bench inference script, changing the dataset to `nebius/SWE-rebench-leaderboard` and selecting the split (`test` for leaderboard submission):
```bash
./evaluation/benchmarks/swe_bench/scripts/run_infer.sh \
llm.your_llm HEAD CodeActAgent 30 50 1 nebius/SWE-rebench-leaderboard test
```
Arguments:
* `llm.your_llm` your model configuration key
* `HEAD` commit reference for reproducibility
* `CodeActAgent` agent type
* `10` number of examples to evaluate
* `50` maximum iterations per task (increase if needed)
* `1` number of workers
* `nebius/SWE-rebench-leaderboard` Hugging Face dataset name
* `test` dataset split
**Tip:** To run on the **full 21k dataset**, replace `nebius/SWE-rebench-leaderboard` with `nebius/SWE-rebench`.
## Evaluating Results
After inference completes, evaluate using the [SWE-bench-fork evaluation harness](https://github.com/SWE-rebench/SWE-bench-fork).
1. Convert the OpenHands output to SWE-bench evaluation format:
```bash
python evaluation/benchmarks/swe_bench/scripts/live/convert.py \
--output_jsonl path/to/evaluation/output.jsonl > preds.jsonl
```
2. Clone the SWE-bench-fork repo (https://github.com/SWE-rebench/SWE-bench-fork) and follow its README to install dependencies.
3. Run the evaluation using the fork:
```bash
python -m swebench.harness.run_evaluation \
--dataset_name nebius/SWE-rebench-leaderboard \
--split test \
--predictions_path preds.jsonl \
--max_workers 10 \
--run_id openhands
```
## Citation
```bibtex
@article{badertdinov2025swerebench,
title={SWE-rebench: An Automated Pipeline for Task Collection and Decontaminated Evaluation of Software Engineering Agents},
author={Badertdinov, Ibragim and Golubev, Alexander and Nekrashevich, Maksim and Shevtsov, Anton and Karasik, Simon and Andriushchenko, Andrei and Trofimova, Maria and Litvintseva, Daria and Yangel, Boris},
journal={arXiv preprint arXiv:2505.20411},
year={2025}
}
```

View File

@@ -1,11 +1,8 @@
"""
Utilities for handling binary files and patch generation in SWE-bench evaluation.
"""
"""Utilities for handling binary files and patch generation in SWE-bench evaluation."""
def remove_binary_diffs(patch_text):
"""
Remove binary file diffs from a git patch.
"""Remove binary file diffs from a git patch.
Args:
patch_text (str): The git patch text
@@ -36,8 +33,7 @@ def remove_binary_diffs(patch_text):
def remove_binary_files_from_git():
"""
Generate a bash command to remove binary files from git staging.
"""Generate a bash command to remove binary files from git staging.
Returns:
str: A bash command that removes binary files from git staging

View File

@@ -26,7 +26,7 @@ from evaluation.utils.shared import (
from openhands.core.config import (
LLMConfig,
OpenHandsConfig,
get_parser,
get_evaluation_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
@@ -111,8 +111,7 @@ def process_instance(
runtime_failure_count: int = 0,
conditional_imports: ConditionalImports | None = None,
) -> EvalOutput:
"""
Evaluate agent performance on a SWE-bench problem instance.
"""Evaluate agent performance on a SWE-bench problem instance.
Note that this signature differs from the expected input to `run_evaluation`. Use
`functools.partial` to provide optional arguments before passing to the evaluation harness.
@@ -353,7 +352,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--input-file',
type=str,

View File

@@ -16,8 +16,7 @@ from openhands.core.logger import openhands_logger as logger
class LocEvaluator:
def __init__(self, args):
"""
Localization evaluation.
"""Localization evaluation.
Args:
args: all main arguments
@@ -76,8 +75,7 @@ class LocEvaluator:
self.task_resolved = False
def _init_dir(self, directory_path):
"""
Check if a directory exists and create it if it doesn't.
"""Check if a directory exists and create it if it doesn't.
Args:
directory_path (str): Path to the directory to check/create
@@ -207,8 +205,7 @@ class LocEvaluator:
self._compute_avg_over_all()
def _write_to_json(self, data, file_name):
"""
Writes the current object data to a JSON file.
"""Writes the current object data to a JSON file.
Returns:
bool: True if writing was successful, False otherwise.
@@ -225,8 +222,7 @@ class LocEvaluator:
return False
def read_from_json(self, file_path):
"""
Reads data from a JSON file and loads it into the current object.
"""Reads data from a JSON file and loads it into the current object.
Returns:
dict: The loaded JSON data, or an empty dict if the file doesn't exist
@@ -253,8 +249,7 @@ class LocEvaluator:
return {}
def read_from_jsonl(self, file_path):
"""
Reads data from a JSON file and loads it into the current object.
"""Reads data from a JSON file and loads it into the current object.
Returns:
dict: The loaded JSON data, or an empty dict if the file doesn't exist
@@ -294,8 +289,7 @@ class LocEvaluator:
history_idx += 1
def _parse_string_to_dict(self, dict_string) -> dict:
"""
Convert a string representation of a dictionary to an actual dictionary.
"""Convert a string representation of a dictionary to an actual dictionary.
Args:
dict_string (str): String representation of a dictionary
@@ -328,8 +322,7 @@ class LocEvaluator:
return None
def _parse_value_from_args(self, argument_str: str, key: str) -> str:
"""
Parse a specific key's value from argument string.
"""Parse a specific key's value from argument string.
Args:
argument_str (str): The argument string containing key-value pairs
@@ -407,8 +400,7 @@ class LocEvaluator:
return ''
def _parse_path_from_args(self, argument_str: str) -> str:
"""
Parse path from argument string.
"""Parse path from argument string.
Args:
argument_str (str): The argument string containing path information
@@ -419,8 +411,7 @@ class LocEvaluator:
return self._parse_value_from_args(argument_str, 'path')
def _parse_func_names_from_str(self, code_patch) -> list:
"""
Parse function names from the new_str code patch.
"""Parse function names from the new_str code patch.
Args:
code_patch: Either a string (argument string) or already extracted new_str code
@@ -801,8 +792,7 @@ class LocEvaluator:
def swe_data_loader(args):
"""
Loading SWE-Bench data.
"""Loading SWE-Bench data.
Args:
args: Main arguments.
@@ -834,8 +824,7 @@ def swe_data_loader(args):
def infer_data_loader(args):
"""
Load instance IDs.
"""Load instance IDs.
Args:
args: Main arguments.
@@ -868,8 +857,7 @@ def infer_data_loader(args):
def infer_cost_calculator(args):
"""
Calculate total and average costs from metric JSON files with detailed output.
"""Calculate total and average costs from metric JSON files with detailed output.
Args:
args: Main arguments.

View File

@@ -28,8 +28,7 @@ class LocalizationInfo:
hunks_per_file: dict[str, int] # File -> number of hunks
def to_dict(self) -> dict[str, Any]:
"""
Convert LocalizationInfo to a dictionary for JSON serialization.
"""Convert LocalizationInfo to a dictionary for JSON serialization.
Returns:
Dictionary representation of the localization information
@@ -58,8 +57,7 @@ class LocalizationInfo:
@classmethod
def from_dict(cls, data: dict[str, Any]) -> 'LocalizationInfo':
"""
Create LocalizationInfo from a dictionary (for loading from JSON).
"""Create LocalizationInfo from a dictionary (for loading from JSON).
Args:
data: Dictionary containing localization information
@@ -91,8 +89,7 @@ class LocalizationInfo:
class LocMeta:
"""
SWE-Bench dataset loader and ground-truth localization parser.
"""SWE-Bench dataset loader and ground-truth localization parser.
This class handles loading SWE-Bench datasets and extracting ground-truth
localization information from patches for code localization evaluation.
@@ -104,8 +101,7 @@ class LocMeta:
dataset_name: str = 'princeton-nlp/SWE-bench_Verified',
split: str = 'test',
):
"""
Initialize LocMeta with a SWE-Bench dataset.
"""Initialize LocMeta with a SWE-Bench dataset.
Args:
dataset_name: HuggingFace dataset name (e.g., "princeton-nlp/SWE-bench_Verified")
@@ -124,8 +120,7 @@ class LocMeta:
self._init_swe_dataset()
def _init_swe_dataset(self) -> None:
"""
Load and initialize the SWE-Bench dataset from HuggingFace.
"""Load and initialize the SWE-Bench dataset from HuggingFace.
Converts to pandas DataFrame for easy manipulation.
"""
try:
@@ -150,8 +145,7 @@ class LocMeta:
raise
def get_instance_by_id(self, instance_id: str) -> pd.Series:
"""
Retrieve a specific instance by its ID.
"""Retrieve a specific instance by its ID.
Args:
instance_id: The instance identifier
@@ -169,8 +163,7 @@ class LocMeta:
return self.df.iloc[idx]
def parse_instance_loc(self, instance: Union[pd.Series, str]) -> LocalizationInfo:
"""
Parse ground-truth localization information from a SWE-Bench instance.
"""Parse ground-truth localization information from a SWE-Bench instance.
Args:
instance: Either a pandas Series with instance data or an instance_id string
@@ -218,8 +211,7 @@ class LocMeta:
def _parse_file_patch_lines(
self, file_patch: str
) -> tuple[list[tuple[int, int]], int, int]:
"""
Parse line ranges and count changes from a single file patch.
"""Parse line ranges and count changes from a single file patch.
Args:
file_patch: Patch content for a single file
@@ -253,8 +245,7 @@ class LocMeta:
def _parse_code_structures_from_patch(
self, file_patch: str, file_path: str
) -> tuple[list[str], list[str]]:
"""
Extract function and class names from patch context (fallback method).
"""Extract function and class names from patch context (fallback method).
Args:
file_patch: Patch content for a single file
@@ -311,8 +302,7 @@ class LocMeta:
def _parse_patch_localization(
self, patch_content: str, instance_id: str
) -> LocalizationInfo:
"""
Parse localization information from a git patch (improved method).
"""Parse localization information from a git patch (improved method).
Args:
patch_content: The git patch content
@@ -390,8 +380,7 @@ class LocMeta:
def _extract_code_structures_from_patch(
self, file_patch: str, file_path: str
) -> tuple[list[str], list[str]]:
"""
Extract function and class names from patch context and content.
"""Extract function and class names from patch context and content.
Args:
file_patch: Patch content for a single file
@@ -519,8 +508,7 @@ class LocMeta:
def _parse_patch_localization_with_runtime(
self, patch_content: str, instance_id: str, runtime: Runtime
) -> LocalizationInfo:
"""
Parse localization information from a git patch using OpenHands runtime.
"""Parse localization information from a git patch using OpenHands runtime.
This is the superior method when runtime is available.
Args:
@@ -596,8 +584,7 @@ class LocMeta:
def parse_instance_loc_with_runtime(
self, instance: Union[pd.Series, str], runtime: Runtime = None
) -> LocalizationInfo:
"""
Parse ground-truth localization information using OpenHands runtime.
"""Parse ground-truth localization information using OpenHands runtime.
Args:
instance: Either a pandas Series with instance data or an instance_id string
@@ -634,8 +621,7 @@ class LocMeta:
def _analyze_source_code_with_runtime(
self, runtime: Runtime, file_path: str, affected_lines: list[int]
) -> tuple[list[str], list[str], dict[int, str], dict[int, str]]:
"""
Analyze source code using OpenHands runtime to find functions and classes.
"""Analyze source code using OpenHands runtime to find functions and classes.
Args:
runtime: OpenHands runtime object
@@ -695,8 +681,7 @@ class LocMeta:
def _parse_cython_content_with_line_mapping(
self, content: str, affected_lines: list[int]
) -> tuple[list[str], list[str], dict[int, str], dict[int, str]]:
"""
Parse Cython content to extract functions and classes with line mapping.
"""Parse Cython content to extract functions and classes with line mapping.
Since Cython files can't be parsed with Python's AST, we use regex-based parsing.
Args:
@@ -828,8 +813,7 @@ class LocMeta:
def _parse_python_content_with_line_mapping(
self, content: str, affected_lines: list[int]
) -> tuple[list[str], list[str], dict[int, str], dict[int, str]]:
"""
Parse Python content to extract functions and classes with accurate line mapping.
"""Parse Python content to extract functions and classes with accurate line mapping.
Args:
content: Python source code content
@@ -914,8 +898,7 @@ class LocMeta:
def _parse_python_content(
self, content: str, affected_lines: list[int]
) -> tuple[list[str], list[str], dict[int, str], dict[int, str]]:
"""
Parse Python content to extract functions and classes.
"""Parse Python content to extract functions and classes.
Args:
content: Python source code content
@@ -989,8 +972,7 @@ class LocMeta:
return [], [], {}, {}
def _split_patch_by_files(self, patch_content: str) -> dict[str, str]:
"""
Split a multi-file patch into individual file patches.
"""Split a multi-file patch into individual file patches.
Args:
patch_content: Complete patch content
@@ -1049,8 +1031,7 @@ class LocMeta:
def _empty_localization_info(
self, instance_id: str = 'unknown'
) -> LocalizationInfo:
"""
Return an empty LocalizationInfo object.
"""Return an empty LocalizationInfo object.
Args:
instance_id: Instance identifier
@@ -1072,8 +1053,7 @@ class LocMeta:
)
def get_dataset_statistics(self) -> dict[str, Any]:
"""
Get statistics about the loaded dataset.
"""Get statistics about the loaded dataset.
Returns:
Dictionary containing dataset statistics
@@ -1095,8 +1075,7 @@ class LocMeta:
return stats
def get_instances_by_repo(self, repo_name: str) -> pd.DataFrame:
"""
Get all instances for a specific repository.
"""Get all instances for a specific repository.
Args:
repo_name: Repository name (e.g., "django/django")

View File

@@ -1,65 +0,0 @@
<uploaded_files>
/workspace/{{ workspace_dir_name }}
</uploaded_files>
I've uploaded a python code repository in the directory {{ workspace_dir_name }}. Consider the following issue description:
<issue_description>
{{ instance.problem_statement }}
</issue_description>
Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?
I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
Your task is to make the minimal changes to non-test files in the /workspace/{{ workspace_dir_name }} directory to ensure the <issue_description> is satisfied.
Follow these phases to resolve the issue:
Phase 1. READING: read the problem and reword it in clearer terms
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
1.2 Hightlight message errors, method names, variables, file names, stack traces, and technical details.
1.3 Explain the problem in clear terms.
1.4 Enumerate the steps to reproduce the problem.
1.5 Hightlight any best practices to take into account when testing and fixing the issue
Phase 2. RUNNING: install and run the tests on the repository
2.1 Follow the readme
2.2 Install the environment and anything needed
2.2 Iterate and figure out how to run the tests
Phase 3. EXPLORATION: find the files that are related to the problem and possible solutions
3.1 Use `grep` to search for relevant methods, classes, keywords and error messages.
3.2 Identify all files related to the problem statement.
3.3 Propose the methods and files to fix the issue and explain why.
3.4 From the possible file locations, select the most likely location to fix the issue.
Phase 4. TEST CREATION: before implementing any fix, create a script to reproduce and verify the issue.
4.1 Look at existing test files in the repository to understand the test format/structure.
4.2 Create a minimal reproduction script that reproduces the located issue.
4.3 Run the reproduction script to confirm you are reproducing the issue.
4.4 Adjust the reproduction script as necessary.
Phase 5. FIX ANALYSIS: state clearly the problem and how to fix it
5.1 State clearly what the problem is.
5.2 State clearly where the problem is located.
5.3 State clearly how the test reproduces the issue.
5.4 State clearly the best practices to take into account in the fix.
5.5 State clearly how to fix the problem.
Phase 6. FIX IMPLEMENTATION: Edit the source code to implement your chosen solution.
6.1 Make minimal, focused changes to fix the issue.
Phase 7. VERIFICATION: Test your implementation thoroughly.
7.1 Run your reproduction script to verify the fix works.
7.2 Add edge cases to your test script to ensure comprehensive coverage.
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
8. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {{ instance.base_commit }}.
8.1 Ensure you've fully addressed all requirements.
8.2 Run any tests in the repository related to:
8.2.1 The issue you are fixing
8.2.2 The files you modified
8.2.3 The functions you changed
8.3 If any tests fail, revise your implementation until all tests pass
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.

View File

@@ -1,45 +0,0 @@
# Task: Fix Issue in Python Repository
## Repository Context
You are provided with a Python code repository that contains an issue requiring your attention. The repository is located in a sandboxed environment, and you have access to the codebase to implement the necessary changes.
The code repository is located at: `/workspace/{{ workspace_dir_name }}`
(This path is provided for context; use file system tools to confirm paths before access).
## Goal
Your goal is to fix the issue described in the **Issue Description** section below. Implement the necessary changes to **non-test files only** within the repository, ensuring that **all relevant tests pass** after your changes.
## Key Requirements & Constraints
1. **Understand the problem** very well: it is a bug report, and you know humans don't always write good descriptions. Explore the codebase to understand the related code and the problem in depth. It is possible that the solution needs to be a bit more extensive than just the stated text. Don't exagerate though: don't do unrelated refactoring, but also don't interpret the description too strictly.
2. **Focus on the issues:** Implement the fix focusing on non-test files related to the issue.
2. **Environment Ready:** The Python environment is pre-configured with all dependencies. Do not install packages.
3. **Mandatory Testing Procedure:**
* **Create Test to Reproduce the Issue:** *Before* implementing any fix, you MUST create a *new test* (separate from existing tests) that specifically reproduces the issue.
* Take existing tests as example to understand the testing format/structure.
* Enhance this test with edge cases.
* Run this test to confirm reproduction.
* **Verify Fix:** After implementing the fix, run your test again to verify the issue is resolved.
* **Identify ALL Relevant Tests:** You MUST perform a **dedicated search and analysis** to identify **all** existing unit tests potentially affected by your changes. This includes:
* Tests in the same module/directory as the changed files (e.g., `tests/` subdirectories).
* Tests explicitly importing or using the modified code/classes/functions.
* Tests mentioned in the issue description or related documentation.
* Tests covering functionalities that *depend on* the modified code (analyze callers/dependencies if necessary).
**If you cannot confidently identify a specific subset, you MUST identify and plan to run the entire test suite for the modified application or module(s). State your identified test scope clearly.**
* **Run Identified Relevant Tests:** You MUST execute the **complete set** of relevant existing unit tests you identified in the previous step. Ensure you are running the *correct and comprehensive set* of tests. You MUST NOT modify these existing tests.
* **Final Check & Verification:** Before finishing, ensure **all** identified relevant existing tests pass. **Explicitly confirm that you have considered potential omissions in your test selection and believe the executed tests comprehensively cover the impact of your changes.** Failing to identify and run the *complete* relevant set constitutes a failure. If any identified tests fail, revise your fix. Passing all relevant tests is the primary measure of success.
4. **Defensive Programming:** Actively practice defensive programming: anticipate and handle potential edge cases, unexpected inputs, and different ways the affected code might be called **to ensure the fix works reliably and allows relevant tests to pass.** Analyze the potential impact on other parts of the codebase.
5. **Final Review:** Compare your solution against the original issue and the base commit ({{ instance.base_commit }}) to ensure completeness and test passage.
## General Workflow Guidance
* Prioritize understanding the problem, exploring the code, planning your fix, implementing it carefully using the required diff format, and **thoroughly testing** according to the **Mandatory Testing Procedure**.
* Consider trade-offs between different solutions. The goal is a **robust change that makes the relevant tests pass.** Quality, correctness, and reliability are key.
* Actively practice defensive programming: anticipate and handle potential edge cases, unexpected inputs, and different ways the affected code might be called **to ensure the fix works reliably and allows relevant tests to pass.** Analyze the potential impact on other parts of the codebase.
* IMPORTANT: Your solution will be tested by additional hidden tests, so do not assume the task is complete just because visible tests pass! Refine the solution until you are confident that it is robust and comprehensive according to the **Defensive Programming** requirement.
## Final Note
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
## Issue Description
{{ instance.problem_statement }}

View File

@@ -43,8 +43,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
@@ -80,6 +80,8 @@ def set_dataset_type(dataset_name: str) -> str:
DATASET_TYPE = 'SWE-Gym'
elif 'swe-bench-live' in name_lower:
DATASET_TYPE = 'SWE-bench-Live'
elif 'swe-rebench' in name_lower:
DATASET_TYPE = 'SWE-rebench'
elif 'multimodal' in name_lower:
DATASET_TYPE = 'Multimodal'
else:
@@ -109,9 +111,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageActio
if mode.startswith('swt'):
template_name = 'swt.j2'
elif mode == 'swe':
if 'claude' in llm_model:
template_name = 'swe_default.j2'
elif 'gpt-4.1' in llm_model:
if 'gpt-4.1' in llm_model:
template_name = 'swe_gpt4.j2'
else:
template_name = (
@@ -180,6 +180,8 @@ def get_instance_docker_image(
docker_image_prefix = 'docker.io/starryzhang/'
elif DATASET_TYPE == 'SWE-bench':
docker_image_prefix = 'docker.io/swebench/'
elif DATASET_TYPE == 'SWE-rebench':
docker_image_prefix = 'docker.io/swerebench/'
repo, name = instance_id.split('__')
image_name = f'{docker_image_prefix.rstrip("/")}/sweb.eval.x86_64.{repo}_1776_{name}:latest'.lower()
logger.debug(f'Using official SWE-Bench image: {image_name}')
@@ -320,6 +322,8 @@ def initialize_runtime(
# inject the instance swe entry
if DATASET_TYPE == 'SWE-bench-Live':
entry_script_path = 'instance_swe_entry_live.sh'
elif DATASET_TYPE == 'SWE-rebench':
entry_script_path = 'instance_swe_entry_rebench.sh'
else:
entry_script_path = 'instance_swe_entry.sh'
runtime.copy_to(
@@ -732,7 +736,7 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,

View File

@@ -28,8 +28,8 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
@@ -201,7 +201,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,

View File

@@ -31,8 +31,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -644,7 +644,7 @@ SWEGYM_EXCLUDE_IDS = [
]
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,

View File

@@ -6,8 +6,7 @@ from openhands.core.logger import openhands_logger as logger
def verify_instance_costs(row: pd.Series) -> float:
"""
Verifies that the accumulated_cost matches the sum of individual costs in metrics.
"""Verifies that the accumulated_cost matches the sum of individual costs in metrics.
Also checks for duplicate consecutive costs which might indicate buggy counting.
If the consecutive costs are identical, the file is affected by this bug:
https://github.com/All-Hands-AI/OpenHands/issues/5383

View File

@@ -1,46 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
INPUT_FILE=$1
NUM_WORKERS=$2
DATASET=$3
SPLIT=$4
if [ -z "$INPUT_FILE" ]; then
echo "INPUT_FILE not specified (should be a path to a jsonl file)"
exit 1
fi
if [ -z "$DATASET" ]; then
echo "DATASET not specified, use default princeton-nlp/SWE-bench_Lite"
DATASET="princeton-nlp/SWE-bench_Lite"
fi
if [ -z "$SPLIT" ]; then
echo "SPLIT not specified, use default test"
SPLIT="test"
fi
if [ -z "$NUM_WORKERS" ]; then
echo "NUM_WORKERS not specified, use default 1"
NUM_WORKERS=1
fi
echo "... Evaluating on $INPUT_FILE ..."
COMMAND="poetry run python evaluation/benchmarks/swe_bench/eval_infer.py \
--eval-num-workers $NUM_WORKERS \
--input-file $INPUT_FILE \
--dataset $DATASET \
--split $SPLIT"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
# Run the command
eval $COMMAND
# update the output with evaluation results
poetry run python evaluation/benchmarks/swe_bench/scripts/eval/update_output_with_eval.py $INPUT_FILE

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
source ~/.bashrc
SWEUTIL_DIR=/swe_util
# FIXME: Cannot read SWE_INSTANCE_ID from the environment variable
# SWE_INSTANCE_ID=django__django-11099
if [ -z "$SWE_INSTANCE_ID" ]; then
echo "Error: SWE_INSTANCE_ID is not set." >&2
exit 1
fi
# Read the swe-bench-test-lite.json file and extract the required item based on instance_id
item=$(jq --arg INSTANCE_ID "$SWE_INSTANCE_ID" '.[] | select(.instance_id == $INSTANCE_ID)' $SWEUTIL_DIR/eval_data/instances/swe-bench-instance.json)
if [[ -z "$item" ]]; then
echo "No item found for the provided instance ID."
exit 1
fi
WORKSPACE_NAME=$(echo "$item" | jq -r '(.repo | tostring) + "__" + (.version | tostring) | gsub("/"; "__")')
echo "WORKSPACE_NAME: $WORKSPACE_NAME"
# Clear the workspace
if [ -d /workspace ]; then
rm -rf /workspace/*
else
mkdir /workspace
fi
# Copy repo to workspace
if [ -d /workspace/$WORKSPACE_NAME ]; then
rm -rf /workspace/$WORKSPACE_NAME
fi
mkdir -p /workspace
cp -r /testbed /workspace/$WORKSPACE_NAME
# Activate instance-specific environment
if [ -d /opt/miniconda3 ]; then
. /opt/miniconda3/etc/profile.d/conda.sh
conda activate testbed
fi
export PATH=/opt/conda/envs/testbed/bin:$PATH

View File

@@ -5,8 +5,7 @@ pynguin_ids = ['pydata__xarray-6548-16541', 'pydata__xarray-7003-16557', 'pydata
ids = ['pydata__xarray-3114-16452', 'pydata__xarray-3151-16453', 'pydata__xarray-3156-16454', 'pydata__xarray-3239-16456', 'pydata__xarray-3239-16457', 'pydata__xarray-3239-16458', 'pydata__xarray-3302-16459', 'pydata__xarray-3364-16461', 'pydata__xarray-3677-16471', 'pydata__xarray-3905-16478', 'pydata__xarray-4182-16484', 'pydata__xarray-4248-16486', 'pydata__xarray-4339-16487', 'pydata__xarray-4419-16488', 'pydata__xarray-4629-16492', 'pydata__xarray-4750-16496', 'pydata__xarray-4802-16505', 'pydata__xarray-4966-16515', 'pydata__xarray-4994-16516', 'pydata__xarray-5033-16517', 'pydata__xarray-5126-16518', 'pydata__xarray-5126-16519', 'pydata__xarray-5131-16520', 'pydata__xarray-5365-16529', 'pydata__xarray-5455-16530', 'pydata__xarray-5662-16532', 'pydata__xarray-5731-16534', 'pydata__xarray-6135-16535', 'pydata__xarray-6135-16536', 'pydata__xarray-6386-16537', 'pydata__xarray-6394-16538', 'pydata__xarray-6400-16539', 'pydata__xarray-6461-16540', 'pydata__xarray-6548-16541', 'pydata__xarray-6599-16543', 'pydata__xarray-6601-16544', 'pydata__xarray-6882-16548', 'pydata__xarray-6889-16549', 'pydata__xarray-7003-16557', 'pydata__xarray-7147-16571', 'pydata__xarray-7150-16572', 'pydata__xarray-7203-16577', 'pydata__xarray-7229-16578', 'pydata__xarray-7393-16581', 'pydata__xarray-7400-16582']
Command eval (our approach):
poetry run ./evaluation/benchmarks/testgeneval/scripts/eval_infer_remote.sh evaluation/evaluation_outputs/outputs/kjain14__testgeneval-test/CodeActAgent/gpt-4o_maxiter_25_N_v0.20.0-no-hint-run_1/output.jsonl 10 kjain14/testgeneval test true
Command run (our approach):
./evaluation/benchmarks/testgeneval/scripts/run_infer.sh llm.eval_gpt HEAD CodeActAgent -1 25 10 kjain14/testgeneval test 1 ../TestGenEval/results/testgeneval/preds/gpt-4o-2024-08-06__testgeneval__0.2__test.jsonl

View File

@@ -181,9 +181,7 @@ def distinct_methods_stats(tree, num_lines):
def loops_stats(tree, num_lines):
"""
Calculate the average number of loops.
"""
"""Calculate the average number of loops."""
total_loops = 0
def traverse(node):
@@ -199,9 +197,7 @@ def loops_stats(tree, num_lines):
def branches_stats(tree, num_lines):
"""
Calculate the average number of branches (conditional statements).
"""
"""Calculate the average number of branches (conditional statements)."""
total_branches = 0
def traverse(node):

View File

@@ -41,7 +41,7 @@ from evaluation.utils.shared import (
reset_logger_for_multiprocessing,
run_evaluation,
)
from openhands.core.config import OpenHandsConfig, SandboxConfig, get_parser
from openhands.core.config import OpenHandsConfig, SandboxConfig, get_evaluation_parser
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
from openhands.events.action import CmdRunAction
@@ -192,8 +192,7 @@ def run_mutation_testing(
def grade_test_output(
test_suite: str, instance: pd.Series, test_output: str, test_spec: TestSpec, runtime
):
"""
Two-pass test grading with short-circuiting:
"""Two-pass test grading with short-circuiting:
1. Run all tests to identify passing/failing tests
2. If no failing tests, evaluate coverage immediately
3. Otherwise, run only passing tests for coverage analysis
@@ -280,8 +279,7 @@ def process_instance(
reset_logger: bool = True,
log_dir: str | None = None,
) -> EvalOutput:
"""
Evaluate agent performance on a TestGenEval problem instance.
"""Evaluate agent performance on a TestGenEval problem instance.
Note that this signature differs from the expected input to `run_evaluation`. Use
`functools.partial` to provide optional arguments before passing to the evaluation harness.
@@ -453,8 +451,7 @@ def process_instance(
def count_and_log_fields(evaluated_predictions, fields, key):
"""
Count and log the sum of specified fields in the evaluated predictions,
"""Count and log the sum of specified fields in the evaluated predictions,
ignoring fields with a value of -1. If all values for a field are -1,
return -1.
@@ -484,7 +481,7 @@ def count_and_log_fields(evaluated_predictions, fields, key):
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--input-file', type=str, required=True, help='Path to input predictions file'
)

View File

@@ -4,8 +4,7 @@ from evaluation.benchmarks.testgeneval.constants import TestStatus
def parse_log_pytest(log: str) -> dict[str, str]:
"""
Parser for test logs generated with PyTest framework
"""Parser for test logs generated with PyTest framework
Args:
log (str): log content
@@ -26,8 +25,7 @@ def parse_log_pytest(log: str) -> dict[str, str]:
def parse_log_pytest_options(log: str) -> dict[str, str]:
"""
Parser for test logs generated with PyTest framework with options
"""Parser for test logs generated with PyTest framework with options
Args:
log (str): log content
@@ -61,8 +59,7 @@ def parse_log_pytest_options(log: str) -> dict[str, str]:
def parse_log_django(log: str) -> dict[str, str]:
"""
Parser for test logs generated with Django tester framework
"""Parser for test logs generated with Django tester framework
Args:
log (str): log content
@@ -141,8 +138,7 @@ def parse_log_django(log: str) -> dict[str, str]:
def parse_log_pytest_v2(log: str) -> dict[str, str]:
"""
Parser for test logs generated with PyTest framework (Later Version)
"""Parser for test logs generated with PyTest framework (Later Version)
Args:
log (str): log content
@@ -170,8 +166,7 @@ def parse_log_pytest_v2(log: str) -> dict[str, str]:
def parse_log_seaborn(log: str) -> dict[str, str]:
"""
Parser for test logs generated with seaborn testing framework
"""Parser for test logs generated with seaborn testing framework
Args:
log (str): log content
@@ -196,8 +191,7 @@ def parse_log_seaborn(log: str) -> dict[str, str]:
def parse_log_sympy(log: str) -> dict[str, str]:
"""
Parser for test logs generated with Sympy framework
"""Parser for test logs generated with Sympy framework
Args:
log (str): log content
@@ -229,8 +223,7 @@ def parse_log_sympy(log: str) -> dict[str, str]:
def parse_log_matplotlib(log: str) -> dict[str, str]:
"""
Parser for test logs generated with PyTest framework
"""Parser for test logs generated with PyTest framework
Args:
log (str): log content

View File

@@ -12,8 +12,7 @@ if sys.getrecursionlimit() < 10_000:
def bleu(gold: list[str], pred: list[str]) -> float:
"""
Calculate BLEU score, using smoothing method 2 with auto reweighting, in the range of 0~100.
"""Calculate BLEU score, using smoothing method 2 with auto reweighting, in the range of 0~100.
:param gold: list of gold tokens
:param pred: list of predicted tokens
@@ -30,8 +29,7 @@ def bleu(gold: list[str], pred: list[str]) -> float:
def batch_bleu(golds: list[list[str]], preds: list[list[str]]) -> list[float]:
"""
Calculate BLEU score for a batch of sentences.
"""Calculate BLEU score for a batch of sentences.
:param golds: list of gold sentences
:param preds: list of predicted sentences
@@ -43,8 +41,7 @@ def batch_bleu(golds: list[list[str]], preds: list[list[str]]) -> list[float]:
def corpus_bleu(golds: list[list[str]], preds: list[list[str]]) -> float:
"""
Calculate corpus-level BLEU score for a batch of sentences.
"""Calculate corpus-level BLEU score for a batch of sentences.
:param golds: list of gold sentences
:param preds: list of predicted sentences
@@ -63,8 +60,7 @@ def corpus_bleu(golds: list[list[str]], preds: list[list[str]]) -> float:
def edit_sim(
gold: Union[str, list[str]], pred: Union[str, list[str]], sep: str = ' '
) -> float:
"""
Calculate char-level edit similarity, in the range of 0~100.
"""Calculate char-level edit similarity, in the range of 0~100.
:param gold: gold sentence or list of gold tokens
:param pred: predicted sentence or list of predicted tokens
@@ -85,8 +81,7 @@ def batch_edit_sim(
preds: list[Union[str, list[str]]],
sep: str = ' ',
) -> list[float]:
"""
Calculate char-level edit similarity for a batch of sentences.
"""Calculate char-level edit similarity for a batch of sentences.
:param golds: list of gold sentences
:param preds: list of predicted sentences
@@ -102,8 +97,7 @@ T = TypeVar('T')
def exact_match(gold: T, pred: T) -> float:
"""
Calculate exact match accuracy, in the range of {0, 100}.
"""Calculate exact match accuracy, in the range of {0, 100}.
:param gold: gold sentence or list of gold tokens
:param pred: predicted sentence or list of predicted tokens
@@ -115,8 +109,7 @@ def exact_match(gold: T, pred: T) -> float:
def batch_exact_match(golds: list[T], preds: list[T]) -> list[float]:
"""
Calculate exact match accuracy for a batch of sentences.
"""Calculate exact match accuracy for a batch of sentences.
:param golds: list of gold sentences
:param preds: list of predicted sentences
@@ -130,8 +123,7 @@ def batch_exact_match(golds: list[T], preds: list[T]) -> list[float]:
def rouge_l(
gold: Union[str, list[str]], pred: Union[str, list[str]], sep: str = ' '
) -> dict[str, float]:
"""
Calculate ROUGE-L F1, precision, and recall scores, in the range of 0~100.
"""Calculate ROUGE-L F1, precision, and recall scores, in the range of 0~100.
:param gold: gold sentence or list of gold tokens
:param pred: predicted sentence or list of predicted tokens
@@ -156,8 +148,7 @@ def batch_rouge_l(
preds: list[Union[str, list[str]]],
sep: str = ' ',
) -> dict[str, list[float]]:
"""
Calculate ROUGE-L F1, precision, and recall scores for a batch of sentences.
"""Calculate ROUGE-L F1, precision, and recall scores for a batch of sentences.
:param golds: list of gold sentences
:param preds: list of predicted sentences
@@ -175,8 +166,7 @@ def accuracy(
pred: list[str],
ignore: Optional[Sequence[str]] = None,
) -> float:
"""
Calculate token-level accuracy, in the range of 0~100.
"""Calculate token-level accuracy, in the range of 0~100.
If gold and pred are not the same length, the longer one would be truncated.
:param gold: list of gold tokens
@@ -210,8 +200,7 @@ def batch_accuracy(
preds: list[list[str]],
ignore: Optional[Sequence[str]] = None,
) -> list[float]:
"""
Calculate token-level accuracy for a batch of sentences.
"""Calculate token-level accuracy for a batch of sentences.
:param golds: list of gold sentences
:param preds: list of predicted sentences
@@ -226,8 +215,7 @@ def batch_accuracy(
def first_match_to_topk(
first_match_list: list[int], k_values: list[int]
) -> dict[int, list[float]]:
"""
Calculate top-k accuracy with the first match ranks (1-indexed).
"""Calculate top-k accuracy with the first match ranks (1-indexed).
:param first_match: first match ranks (1-indexed)
:param k_values: k values to consider
@@ -237,8 +225,7 @@ def first_match_to_topk(
def pass_at_k(n: int, c: int, k: int) -> float:
"""
Sample pass@k metric according to the Codex paper, but in the scale of 0~100.
"""Sample pass@k metric according to the Codex paper, but in the scale of 0~100.
:param n: total number of samples
:param c: number of correct samples
:param k: k in pass@$k$
@@ -251,8 +238,7 @@ def pass_at_k(n: int, c: int, k: int) -> float:
def self_bleu(samples: list[list[str]]) -> float:
"""
Calculate self-BLEU among the samples.
"""Calculate self-BLEU among the samples.
:param samples: the chosen m samples
:return: self-BLEU
"""
@@ -274,8 +260,7 @@ def self_bleu(samples: list[list[str]]) -> float:
def self_edit_distance(samples: list[Union[str, list[str]]], sep=' ') -> float:
"""
Calculate self-edit-distance among the samples.
"""Calculate self-edit-distance among the samples.
:param samples: the chosen m samples
:param sep: the separator between tokens
:return: self-edit-distance

View File

@@ -30,8 +30,7 @@ def check_mutation(mutation_output):
def count_methods(code_str):
"""
Counts the number of methods/functions in a given string of code.
"""Counts the number of methods/functions in a given string of code.
Args:
code_str (str): A string containing code.
@@ -46,8 +45,7 @@ def count_methods(code_str):
def get_lines_of_code(code_str):
"""
Extracts lines of code from a given string.
"""Extracts lines of code from a given string.
Args:
code_str (str): A string containing code.

View File

@@ -37,8 +37,8 @@ from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
SandboxConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -491,7 +491,7 @@ def prepare_dataset_pre(dataset: pd.DataFrame, filter_column: str) -> pd.DataFra
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,

View File

@@ -7,8 +7,7 @@ import traceback
def insert_line_in_string(input_string, new_str, insert_line):
"""
Inserts a new line into a string at the specified line number.
"""Inserts a new line into a string at the specified line number.
:param input_string: The original string.
:param new_str: The string to insert.
@@ -29,8 +28,7 @@ def insert_line_in_string(input_string, new_str, insert_line):
def print_string_diff(original, modified):
"""
Prints the differences between two strings line by line.
"""Prints the differences between two strings line by line.
:param original: The original string.
:param modified: The modified string.

View File

@@ -37,8 +37,7 @@ def extract_preamble_classes_and_functions(code):
current_position = 0
def extract_class_body(code: str, start_index: int) -> tuple[str, int]:
"""
Extracts the body of a class from the given code starting from the specified index.
"""Extracts the body of a class from the given code starting from the specified index.
Returns the class body and the end index of the class body.
"""
if not code or start_index < 0 or start_index >= len(code):
@@ -168,8 +167,8 @@ def extract_preamble_classes_and_functions(code):
def filter_passing_tests(
test_content: str, test_output: str, repo: str
) -> tuple[str, list[str], list[str]]:
"""
Filter tests based on their execution results.
"""Filter tests based on their execution results.
Returns:
Tuple containing:
- Modified test content with only passing tests
@@ -246,8 +245,7 @@ def filter_passing_tests(
def filter_tests(
test_content: str, test_output: str, repo: str
) -> tuple[str, list[str], list[str]]:
"""
Filter tests using AST parsing to remove failing test functions from the test file.
"""Filter tests using AST parsing to remove failing test functions from the test file.
Non-test functions (e.g. setup or helper methods) and classes (even if all test methods are failing)
are preserved.

View File

@@ -20,9 +20,7 @@ DIFF_MODIFIED_FILE_REGEX = r'--- a/(.*)'
@dataclass
class TestSpec:
"""
A dataclass that represents a test specification for a single instance of SWE-bench.
"""
"""A dataclass that represents a test specification for a single instance of SWE-bench."""
instance_id: str
id: str
@@ -86,10 +84,7 @@ def make_test_setup(specs, env_name, repo_directory, includes_tox=False):
def make_test_script_list(test_cmd, specs, env_name, repo_directory):
"""
Runs the tests.
"""
"""Runs the tests."""
includes_tox = 'tox' in test_cmd
eval_commands = make_test_setup(specs, env_name, repo_directory, includes_tox)
eval_commands += [
@@ -104,10 +99,7 @@ def make_test_script_list(test_cmd, specs, env_name, repo_directory):
def make_mutation_script_list(specs, env_name, repo_directory, mutation_timeout):
"""
Runs the tests.
"""
"""Runs the tests."""
eval_commands = make_test_setup(specs, env_name, repo_directory)
eval_commands += [
'cosmic-ray init mutation.toml mutation.sqlite',

View File

@@ -11,8 +11,7 @@ from evaluation.benchmarks.testgeneval.constants import (
def get_test_directives(instance: TestGenEvalInstance) -> list:
"""
Get test directives from the test_patch of a task instance
"""Get test directives from the test_patch of a task instance
Args:
instance (dict): task instance
@@ -43,9 +42,7 @@ def get_test_directives(instance: TestGenEvalInstance) -> list:
def load_testgeneval_dataset(
name='kjain14/testgeneval', split='test', ids=None
) -> list[TestGenEvalInstance]:
"""
Load SWE-bench dataset from Hugging Face Datasets or local .json/.jsonl file
"""
"""Load SWE-bench dataset from Hugging Face Datasets or local .json/.jsonl file"""
# check that all instance IDs are in the dataset
if ids:
ids = set(ids)

View File

@@ -24,9 +24,7 @@ class ActionType(Enum):
@dataclass
class Selector:
"""
Represents either a direct anchor ID or a descriptive selector
"""
"""Represents either a direct anchor ID or a descriptive selector"""
value: str
is_anchor: bool = False
@@ -149,8 +147,7 @@ def find_matching_anchor(content: str, selector: str) -> str | None:
def resolve_action(action: BrowserAction, content: str) -> BrowserAction:
"""
Resolve any descriptive selectors in the action to anchor IDs based on the content.
"""Resolve any descriptive selectors in the action to anchor IDs based on the content.
Returns a new action with resolved selectors.
"""
if isinstance(action, (InputAction, ClickAction)):
@@ -174,8 +171,7 @@ def pre_login(
save_screenshots=True,
screenshots_dir='screenshots',
):
"""
Logs in to all the websites that are needed for the evaluation.
"""Logs in to all the websites that are needed for the evaluation.
Once logged in, the sessions would be cached in the browser, so OpenHands
agent doesn't need to log in to these websites again.
"""

View File

@@ -18,8 +18,8 @@ from openhands.core.config import (
LLMConfig,
OpenHandsConfig,
get_agent_config_arg,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.config.agent_config import AgentConfig
from openhands.core.logger import openhands_logger as logger
@@ -68,8 +68,7 @@ def get_config(
def load_dependencies(runtime: Runtime) -> list[str]:
"""
Every task has a dependencies.yml file, which lists all the services that the
"""Every task has a dependencies.yml file, which lists all the services that the
task depends on. This function loads the file and returns all dependent service names.
"""
command = 'cat /utils/dependencies.yml'
@@ -197,7 +196,7 @@ def run_evaluator(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--task-image-name',
type=str,

View File

@@ -11,9 +11,7 @@ import sys
def calculate_cost(model: str, prompt_tokens: int, completion_tokens: int) -> float:
"""
Calculate the cost of the model call.
"""
"""Calculate the cost of the model call."""
if 'claude-3-5-sonnet' in model.lower():
# https://www.anthropic.com/pricing#anthropic-api, accessed 12/11/2024
return 0.000003 * prompt_tokens + 0.000015 * completion_tokens
@@ -60,8 +58,7 @@ def calculate_cost(model: str, prompt_tokens: int, completion_tokens: int) -> fl
def analyze_eval_json_file(filepath: str) -> tuple[int, int]:
"""
Analyze a single eval JSON file and extract the total and result from final_score.
"""Analyze a single eval JSON file and extract the total and result from final_score.
Args:
filepath: Path to the JSON file
@@ -84,8 +81,7 @@ def analyze_eval_json_file(filepath: str) -> tuple[int, int]:
def analyze_traj_json_file(filepath: str) -> tuple[int, float]:
"""
Analyze a single trajectory JSON file and extract the steps and tokens
"""Analyze a single trajectory JSON file and extract the steps and tokens
for each step. Then estimate the cost based on the tokens and the model type.
Note: this is assuming there's no prompt caching at all.
"""
@@ -115,8 +111,7 @@ def analyze_traj_json_file(filepath: str) -> tuple[int, float]:
def analyze_folder(
folder_path: str,
) -> tuple[dict[str, tuple[int, int]], dict[str, tuple[int, float]]]:
"""
Analyze all eval_*.json & traj_*.json files in the specified folder.
"""Analyze all eval_*.json & traj_*.json files in the specified folder.
Args:
folder_path: Path to the folder containing JSON files
@@ -148,9 +143,7 @@ def analyze_folder(
def get_task_nature_category(task_name: str) -> str:
"""
Get the nature category of the task.
"""
"""Get the nature category of the task."""
task_nature = task_name.split('-')[0]
if task_nature.lower() in ['sde', 'pm', 'ds', 'admin', 'hr', 'finance']:
return task_nature
@@ -159,8 +152,7 @@ def get_task_nature_category(task_name: str) -> str:
def calculate_score(total: int, result: int) -> float:
"""
Calculate the score as a number between 0 and 1.
"""Calculate the score as a number between 0 and 1.
Formula: score = (result / total) * 0.5 + (result // total) * 0.5
Explanation:
@@ -178,8 +170,7 @@ def calculate_score(total: int, result: int) -> float:
def is_perfect_completion(total: int, result: int) -> bool:
"""
Check if the task achieved perfect completion.
"""Check if the task achieved perfect completion.
Args:
total: Total possible points

View File

@@ -19,8 +19,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -157,7 +157,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,

View File

@@ -1,6 +1,4 @@
"""
GPT performs line level generation prediction and truncates overly long tokens
"""
"""GPT performs line level generation prediction and truncates overly long tokens"""
import json
import os
@@ -56,8 +54,7 @@ def predict(content, model_name):
def bulid_prompt(description, old_version, old_code, new_version) -> str:
"""
build prompt
"""Build prompt
:param version:
:param description:
:param masked_code:

View File

@@ -1,6 +1,4 @@
"""
GPT performs line level generation prediction and truncates overly long tokens
"""
"""GPT performs line level generation prediction and truncates overly long tokens"""
import json
import os
@@ -56,8 +54,7 @@ def predict(content, model_name):
def bulid_prompt(version, description) -> str:
"""
build prompt
"""Build prompt
:param version:
:param description:
:param masked_code:

View File

@@ -1,6 +1,4 @@
"""
block completion
"""
"""block completion"""
import copy
import gc
@@ -79,8 +77,7 @@ def run_inference(model_name, origin_data_list):
def bulid_prompt(version, description) -> str:
"""
build prompt
"""Build prompt
:param version:
:param description:
:param masked_code:

View File

@@ -1,6 +1,4 @@
"""
code migration
"""
"""code migration"""
import copy
import gc
@@ -81,8 +79,7 @@ def run_inference(model_name, origin_data_list):
def bulid_prompt(description, old_version, old_code, new_version) -> str:
"""
build prompt
"""Build prompt
:param version:
:param description:
:param masked_code:

View File

@@ -1,5 +1,4 @@
"""
评测block的预测能力
"""评测block的预测能力
1、判断是否包含正确的函数名
2、判断是否合法
3、计算ISM和PM
@@ -22,8 +21,7 @@ def is_code_valid(code):
def longest_common_prefix_between_lists_with_elements(list1, list2):
"""
计算两个字符串列表中元素的最长前缀匹配长度
"""计算两个字符串列表中元素的最长前缀匹配长度
:param list1:
:param list2:
:return:
@@ -46,8 +44,7 @@ def longest_common_prefix_between_lists_with_elements(list1, list2):
def get_token(ans_code: str, output_code: str):
"""
对代码进行词法分析,分解成标识符,返回两个标识符列表
"""对代码进行词法分析,分解成标识符,返回两个标识符列表
:param ans_code:
:param output_code:
:return:
@@ -94,8 +91,7 @@ def get_token(ans_code: str, output_code: str):
def get_token_per_line(code: str):
"""
对每一行代码进行词法分析,记录每一行的标识符
"""对每一行代码进行词法分析,记录每一行的标识符
:param code: 代码字符串
:return: 每一行的标识符列表组成的列表
"""
@@ -117,8 +113,7 @@ def get_token_per_line(code: str):
def get_ISM(answer_code: str, model_output_list: list, answer_name: str) -> list:
"""
计算ISM返回一个有序的得分列表
"""计算ISM返回一个有序的得分列表
:return:
"""
score_list = []
@@ -157,8 +152,7 @@ def get_ISM(answer_code: str, model_output_list: list, answer_name: str) -> list
def get_ISM_without_verification(
answer_code: str, model_output_list: list, answer_name: str
) -> list:
"""
计算ISM返回一个有序的得分列表
"""计算ISM返回一个有序的得分列表
:return:
"""
score_list = []
@@ -190,8 +184,7 @@ def get_ISM_without_verification(
def longest_common_prefix_with_lengths(list1, list2):
"""
计算两个二维列表中每个子列表的最长前缀匹配长度,并记录拥有最长前缀匹配长度的两个子列表的长度
"""计算两个二维列表中每个子列表的最长前缀匹配长度,并记录拥有最长前缀匹配长度的两个子列表的长度
:param list1: 第一个二维列表
:param list2: 第二个二维列表
:return: 最长前缀匹配长度以及拥有最长前缀匹配长度的两个子列表的长度
@@ -216,8 +209,7 @@ def longest_common_prefix_with_lengths(list1, list2):
def get_PM(answer_code: str, model_output_list: list, answer_name: str) -> list:
"""
计算PM返回一个有序的得分列表
"""计算PM返回一个有序的得分列表
:return:
"""
score_list = []
@@ -254,8 +246,7 @@ def get_PM(answer_code: str, model_output_list: list, answer_name: str) -> list:
def get_score(score_list: list, k):
"""
计算score@n,k
"""计算score@n,k
:param score_list:
:param k:
:return:

View File

@@ -1,6 +1,4 @@
"""
Calculate the cdc score for migration
"""
"""Calculate the cdc score for migration"""
import json
import math
@@ -11,8 +9,7 @@ import re
def is_correct_parameter_count(function_name, correct_code, test_code):
"""
判断参数数量是否一致
"""判断参数数量是否一致
:param function_name:
:param correct_code:
:param test_code:
@@ -43,8 +40,7 @@ def is_correct_parameter_count(function_name, correct_code, test_code):
def check_keyword_parameters(function_name, correct_code, test_code):
"""
判断关键词参数赋值是否正确使用
"""判断关键词参数赋值是否正确使用
:param function_name:
:param correct_code:
:param test_code:
@@ -82,8 +78,7 @@ def check_keyword_parameters(function_name, correct_code, test_code):
def with_correct(answer_code: str, model_output: str) -> bool:
"""
当answer是with结构时判断模型生成的是不是with结构
"""当answer是with结构时判断模型生成的是不是with结构
:param answer_code:
:param model_output:
:return:
@@ -105,9 +100,7 @@ def compute_block_score_k(
core_line_in_core_block,
core_line_in_output_clear,
):
"""
cdc需要满足五个条件em只需要满足第一个条件
"""
"""cdc需要满足五个条件em只需要满足第一个条件"""
c = 0
n = len(model_output)
for index, code in enumerate(model_output):

View File

@@ -1,6 +1,4 @@
"""
Calculate the cdc score for line and block
"""
"""Calculate the cdc score for line and block"""
import json
import math
@@ -19,8 +17,7 @@ def is_code_valid(code):
def is_correct_parameter_count(function_name, correct_code, test_code):
"""
判断参数数量是否一致
"""判断参数数量是否一致
:param function_name:
:param correct_code:
:param test_code:
@@ -51,8 +48,7 @@ def is_correct_parameter_count(function_name, correct_code, test_code):
def check_keyword_parameters(function_name, correct_code, test_code):
"""
判断关键词参数赋值是否正确使用
"""判断关键词参数赋值是否正确使用
:param function_name:
:param correct_code:
:param test_code:
@@ -90,8 +86,7 @@ def check_keyword_parameters(function_name, correct_code, test_code):
def with_correct(answer_code: str, model_output: str) -> bool:
"""
当answer是with结构时判断模型生成的是不是with结构
"""当answer是with结构时判断模型生成的是不是with结构
:param answer_code:
:param model_output:
:return:

View File

@@ -1,6 +1,4 @@
"""
Calculate the cdc score for line and block
"""
"""Calculate the cdc score for line and block"""
import json
import math
@@ -19,8 +17,7 @@ def is_code_valid(code):
def is_correct_parameter_count(function_name, correct_code, test_code):
"""
判断参数数量是否一致
"""判断参数数量是否一致
:param function_name:
:param correct_code:
:param test_code:
@@ -51,8 +48,7 @@ def is_correct_parameter_count(function_name, correct_code, test_code):
def check_keyword_parameters(function_name, correct_code, test_code):
"""
判断关键词参数赋值是否正确使用
"""判断关键词参数赋值是否正确使用
:param function_name:
:param correct_code:
:param test_code:
@@ -90,8 +86,7 @@ def check_keyword_parameters(function_name, correct_code, test_code):
def with_correct(answer_code: str, model_output: str) -> bool:
"""
当answer是with结构时判断模型生成的是不是with结构
"""当answer是with结构时判断模型生成的是不是with结构
:param answer_code:
:param model_output:
:return:

View File

@@ -1,6 +1,4 @@
"""
Find the line of code generated by the model using the block in the version code
"""
"""Find the line of code generated by the model using the block in the version code"""
import json
import os

View File

@@ -1,6 +1,4 @@
"""
Find the line of code generated by the model using the block in the version code
"""
"""Find the line of code generated by the model using the block in the version code"""
import json
import os

View File

@@ -1,6 +1,4 @@
"""
Clear the<start>and<end>generated by the model in inference
"""
"""Clear the<start>and<end>generated by the model in inference"""
import json

View File

@@ -31,8 +31,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -565,7 +565,7 @@ SWEGYM_EXCLUDE_IDS = [
]
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,

View File

@@ -263,19 +263,20 @@ def prepare_dataset(
f'Randomly sampling {eval_n_limit} unique instances with random seed 42.'
)
def make_serializable(instance: pd.Series) -> dict:
def make_serializable(instance_dict: dict) -> dict:
import numpy as np
instance_dict = instance.to_dict()
for k, v in instance_dict.items():
if isinstance(v, np.ndarray):
instance_dict[k] = v.tolist()
elif isinstance(v, pd.Timestamp):
instance_dict[k] = str(v)
elif isinstance(v, dict):
instance_dict[k] = make_serializable(v)
return instance_dict
new_dataset = [
make_serializable(instance)
make_serializable(instance.to_dict())
for _, instance in dataset.iterrows()
if str(instance[id_column]) not in finished_ids
]
@@ -621,8 +622,7 @@ def compatibility_for_eval_history_pairs(
def is_fatal_evaluation_error(error: str | None) -> bool:
"""
The AgentController class overrides last error for certain exceptions
"""The AgentController class overrides last error for certain exceptions
We want to ensure those exeption do not overlap with fatal exceptions defined here
This is because we do a comparisino against the stringified error
"""

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { FileService } from "#/api/file-service/file-service.api";
import OpenHands from "#/api/open-hands";
import {
FILE_VARIANTS_1,
FILE_VARIANTS_2,
@@ -10,20 +10,20 @@ import {
* You can find the mock handlers in `frontend/src/mocks/file-service-handlers.ts`.
*/
describe("FileService", () => {
describe("OpenHands File API", () => {
it("should get a list of files", async () => {
await expect(FileService.getFiles("test-conversation-id")).resolves.toEqual(
await expect(OpenHands.getFiles("test-conversation-id")).resolves.toEqual(
FILE_VARIANTS_1,
);
await expect(
FileService.getFiles("test-conversation-id-2"),
OpenHands.getFiles("test-conversation-id-2"),
).resolves.toEqual(FILE_VARIANTS_2);
});
it("should get content of a file", async () => {
await expect(
FileService.getFile("test-conversation-id", "file1.txt"),
OpenHands.getFile("test-conversation-id", "file1.txt"),
).resolves.toEqual("Content of file1.txt");
});
});

View File

@@ -3,8 +3,6 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { LaunchMicroagentModal } from "#/components/features/chat/microagent/launch-microagent-modal";
import { MemoryService } from "#/api/memory-service/memory-service.api";
import { FileService } from "#/api/file-service/file-service.api";
import { I18nKey } from "#/i18n/declaration";
vi.mock("react-router", async () => ({

View File

@@ -85,9 +85,10 @@ describe("ConversationPanel", () => {
vi.clearAllMocks();
vi.restoreAllMocks();
// Setup default mock for getUserConversations
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([
...mockConversations,
]);
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue({
results: [...mockConversations],
next_page_id: null,
});
});
it("should render the conversations", async () => {
@@ -101,7 +102,10 @@ describe("ConversationPanel", () => {
it("should display an empty state when there are no conversations", async () => {
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue([]);
getUserConversationsSpy.mockResolvedValue({
results: [],
next_page_id: null,
});
renderConversationPanel();
@@ -195,7 +199,10 @@ describe("ConversationPanel", () => {
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockImplementation(async () => mockData);
getUserConversationsSpy.mockImplementation(async () => ({
results: mockData,
next_page_id: null,
}));
const deleteUserConversationSpy = vi.spyOn(
OpenHands,
@@ -249,7 +256,10 @@ describe("ConversationPanel", () => {
it("should refetch data on rerenders", async () => {
const user = userEvent.setup();
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue([...mockConversations]);
getUserConversationsSpy.mockResolvedValue({
results: [...mockConversations],
next_page_id: null,
});
function PanelWithToggle() {
const [isOpen, setIsOpen] = React.useState(true);
@@ -343,7 +353,10 @@ describe("ConversationPanel", () => {
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue(mockRunningConversations);
getUserConversationsSpy.mockResolvedValue({
results: mockRunningConversations,
next_page_id: null,
});
renderConversationPanel();
@@ -407,7 +420,10 @@ describe("ConversationPanel", () => {
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockImplementation(async () => mockData);
getUserConversationsSpy.mockImplementation(async () => ({
results: mockData,
next_page_id: null,
}));
const stopConversationSpy = vi.spyOn(OpenHands, "stopConversation");
stopConversationSpy.mockImplementation(async (id: string) => {
@@ -492,7 +508,10 @@ describe("ConversationPanel", () => {
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue(mockMixedStatusConversations);
getUserConversationsSpy.mockResolvedValue({
results: mockMixedStatusConversations,
next_page_id: null,
});
renderConversationPanel();

View File

@@ -85,17 +85,36 @@ describe("RepoConnector", () => {
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
// Mock the search function that's used by the dropdown
vi.spyOn(OpenHands, "searchGitRepositories").mockResolvedValue(
MOCK_RESPOSITORIES,
);
renderRepoConnector();
// Wait for the loading state to be replaced with the dropdown
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
await userEvent.click(dropdown);
// First select the provider
const providerDropdown = await waitFor(() =>
screen.getByText("Select Provider"),
);
await userEvent.click(providerDropdown);
await userEvent.click(screen.getByText("Github"));
// Then interact with the repository dropdown
const repoDropdown = await waitFor(() =>
screen.getByTestId("repo-dropdown"),
);
const repoInput = within(repoDropdown).getByRole("combobox");
await userEvent.click(repoInput);
// Wait for the options to be loaded and displayed
await waitFor(() => {
screen.getByText("rbren/polaris");
screen.getByText("All-Hands-AI/OpenHands");
expect(screen.getByText("rbren/polaris")).toBeInTheDocument();
expect(screen.getByText("All-Hands-AI/OpenHands")).toBeInTheDocument();
});
});
@@ -104,18 +123,47 @@ describe("RepoConnector", () => {
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
renderRepoConnector();
const launchButton = await screen.findByTestId("repo-launch-button");
expect(launchButton).toBeDisabled();
// Wait for the loading state to be replaced with the dropdown
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
await userEvent.click(dropdown);
// Mock the repository branches API call
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue([
{ name: "main", commit_sha: "123", protected: false },
{ name: "develop", commit_sha: "456", protected: false },
]);
// First select the provider
const providerDropdown = await waitFor(() =>
screen.getByText("Select Provider"),
);
await userEvent.click(providerDropdown);
await userEvent.click(screen.getByText("Github"));
// Then select the repository
const repoDropdown = await waitFor(() =>
screen.getByTestId("repo-dropdown"),
);
const repoInput = within(repoDropdown).getByRole("combobox");
await userEvent.click(repoInput);
// Wait for the options to be loaded and displayed
await waitFor(() => {
expect(screen.getByText("rbren/polaris")).toBeInTheDocument();
});
await userEvent.click(screen.getByText("rbren/polaris"));
// Wait for the branch to be auto-selected
await waitFor(() => {
expect(screen.getByText("main")).toBeInTheDocument();
});
expect(launchButton).toBeEnabled();
});
@@ -180,7 +228,10 @@ describe("RepoConnector", () => {
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
renderRepoConnector();
@@ -192,14 +243,37 @@ describe("RepoConnector", () => {
// repo not selected yet
expect(createConversationSpy).not.toHaveBeenCalled();
// select a repository from the dropdown
const dropdown = await waitFor(() =>
// Mock the repository branches API call
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue([
{ name: "main", commit_sha: "123", protected: false },
{ name: "develop", commit_sha: "456", protected: false },
]);
// First select the provider
const providerDropdown = await waitFor(() =>
screen.getByText("Select Provider"),
);
await userEvent.click(providerDropdown);
await userEvent.click(screen.getByText("Github"));
// Then select the repository
const repoDropdown = await waitFor(() =>
within(repoConnector).getByTestId("repo-dropdown"),
);
await userEvent.click(dropdown);
const repoInput = within(repoDropdown).getByRole("combobox");
await userEvent.click(repoInput);
// Wait for the options to be loaded and displayed
await waitFor(() => {
expect(screen.getByText("rbren/polaris")).toBeInTheDocument();
});
await userEvent.click(screen.getByText("rbren/polaris"));
// Wait for the branch to be auto-selected
await waitFor(() => {
expect(screen.getByText("main")).toBeInTheDocument();
});
const repoOption = screen.getByText("rbren/polaris");
await userEvent.click(repoOption);
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
@@ -218,17 +292,46 @@ describe("RepoConnector", () => {
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
// Mock the repository branches API call
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue([
{ name: "main", commit_sha: "123", protected: false },
{ name: "develop", commit_sha: "456", protected: false },
]);
renderRepoConnector();
const launchButton = await screen.findByTestId("repo-launch-button");
// Wait for the loading state to be replaced with the dropdown
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
await userEvent.click(dropdown);
// First select the provider
const providerDropdown = await waitFor(() =>
screen.getByText("Select Provider"),
);
await userEvent.click(providerDropdown);
await userEvent.click(screen.getByText("Github"));
// Then select the repository
const repoDropdown = await waitFor(() =>
screen.getByTestId("repo-dropdown"),
);
const repoInput = within(repoDropdown).getByRole("combobox");
await userEvent.click(repoInput);
// Wait for the options to be loaded and displayed
await waitFor(() => {
expect(screen.getByText("rbren/polaris")).toBeInTheDocument();
});
await userEvent.click(screen.getByText("rbren/polaris"));
// Wait for the branch to be auto-selected
await waitFor(() => {
expect(screen.getByText("main")).toBeInTheDocument();
});
await userEvent.click(launchButton);
expect(launchButton).toBeDisabled();
expect(launchButton).toHaveTextContent(/Loading/i);

View File

@@ -12,6 +12,8 @@ const mockUseCreateConversation = vi.fn();
const mockUseIsCreatingConversation = vi.fn();
const mockUseTranslation = vi.fn();
const mockUseAuth = vi.fn();
const mockUseGitRepositories = vi.fn();
const mockUseUserProviders = vi.fn();
// Setup default mock returns
mockUseUserRepositories.mockReturnValue({
@@ -30,6 +32,29 @@ mockUseIsCreatingConversation.mockReturnValue(false);
mockUseTranslation.mockReturnValue({ t: (key: string) => key });
// Default mock for useGitRepositories
mockUseGitRepositories.mockReturnValue({
data: { pages: [] },
isLoading: false,
isError: false,
hasNextPage: false,
isFetchingNextPage: false,
fetchNextPage: vi.fn(),
onLoadMore: vi.fn(),
});
vi.mock("react-i18next", () => ({
useTranslation: () => mockUseTranslation(),
}));
vi.mock("#/hooks/use-user-providers", () => ({
useUserProviders: () => mockUseUserProviders(),
}));
mockUseUserProviders.mockReturnValue({
providers: ["github"],
});
mockUseAuth.mockReturnValue({
isAuthenticated: true,
isLoading: false,
@@ -71,6 +96,10 @@ vi.mock("react-router", async (importActual) => ({
useNavigate: vi.fn(),
}));
vi.mock("#/hooks/query/use-git-repositories", () => ({
useGitRepositories: () => mockUseGitRepositories(),
}));
const mockOnRepoSelection = vi.fn();
const renderForm = () =>
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />, {
@@ -96,34 +125,6 @@ describe("RepositorySelectionForm", () => {
vi.clearAllMocks();
});
it("shows loading indicator when repositories are being fetched", () => {
const MOCK_REPOS: GitRepository[] = [
{
id: "1",
full_name: "user/repo1",
git_provider: "github",
is_public: true,
},
{
id: "2",
full_name: "user/repo2",
git_provider: "github",
is_public: true,
},
];
const retrieveUserGitRepositoriesSpy = vi.spyOn(
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_REPOS);
renderForm();
// Check if loading indicator is displayed
expect(screen.getByTestId("repo-dropdown-loading")).toBeInTheDocument();
expect(screen.getByText("HOME$LOADING_REPOSITORIES")).toBeInTheDocument();
});
it("shows dropdown when repositories are loaded", async () => {
const MOCK_REPOS: GitRepository[] = [
{
@@ -139,24 +140,30 @@ describe("RepositorySelectionForm", () => {
is_public: true,
},
];
const retrieveUserGitRepositoriesSpy = vi.spyOn(
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_REPOS);
mockUseGitRepositories.mockReturnValue({
data: { pages: [{ data: MOCK_REPOS }] },
isLoading: false,
isError: false,
hasNextPage: false,
isFetchingNextPage: false,
fetchNextPage: vi.fn(),
onLoadMore: vi.fn(),
});
renderForm();
expect(await screen.findByTestId("repo-dropdown")).toBeInTheDocument();
});
it("shows error message when repository fetch fails", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockRejectedValue(
new Error("Failed to load"),
);
mockUseGitRepositories.mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
hasNextPage: false,
isFetchingNextPage: false,
fetchNextPage: vi.fn(),
onLoadMore: vi.fn(),
});
renderForm();
@@ -194,40 +201,45 @@ describe("RepositorySelectionForm", () => {
];
const searchGitReposSpy = vi.spyOn(OpenHands, "searchGitRepositories");
const retrieveUserGitRepositoriesSpy = vi.spyOn(
OpenHands,
"retrieveUserGitRepositories",
);
searchGitReposSpy.mockResolvedValue(MOCK_SEARCH_REPOS);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_REPOS);
mockUseGitRepositories.mockReturnValue({
data: { pages: [{ data: MOCK_REPOS }] },
isLoading: false,
isError: false,
hasNextPage: false,
isFetchingNextPage: false,
fetchNextPage: vi.fn(),
onLoadMore: vi.fn(),
});
mockUseAuth.mockReturnValue({
isAuthenticated: true,
isLoading: false,
providersAreSet: true,
user: {
id: 1,
login: "testuser",
avatar_url: "https://example.com/avatar.png",
name: "Test User",
email: "test@example.com",
company: "Test Company",
},
login: vi.fn(),
logout: vi.fn(),
});
renderForm();
const input = await screen.findByTestId("repo-dropdown");
await userEvent.click(input);
for (const repo of MOCK_REPOS) {
expect(screen.getByText(repo.full_name)).toBeInTheDocument();
}
expect(
screen.queryByText(MOCK_SEARCH_REPOS[0].full_name),
).not.toBeInTheDocument();
expect(searchGitReposSpy).not.toHaveBeenCalled();
const dropdown = await screen.findByTestId("repo-dropdown");
const input = dropdown.querySelector('input[type="text"]') as HTMLInputElement;
expect(input).toBeInTheDocument();
await userEvent.type(input, "https://github.com/kubernetes/kubernetes");
expect(searchGitReposSpy).toHaveBeenLastCalledWith(
"kubernetes/kubernetes",
3,
);
expect(
screen.getByText(MOCK_SEARCH_REPOS[0].full_name),
).toBeInTheDocument();
for (const repo of MOCK_REPOS) {
expect(screen.queryByText(repo.full_name)).not.toBeInTheDocument();
}
});
it("should call onRepoSelection when a searched repository is selected", async () => {
@@ -243,20 +255,26 @@ describe("RepositorySelectionForm", () => {
const searchGitReposSpy = vi.spyOn(OpenHands, "searchGitRepositories");
searchGitReposSpy.mockResolvedValue(MOCK_SEARCH_REPOS);
mockUseGitRepositories.mockReturnValue({
data: { pages: [{ data: MOCK_SEARCH_REPOS }] },
isLoading: false,
isError: false,
hasNextPage: false,
isFetchingNextPage: false,
fetchNextPage: vi.fn(),
onLoadMore: vi.fn(),
});
renderForm();
const input = await screen.findByTestId("repo-dropdown");
const dropdown = await screen.findByTestId("repo-dropdown");
const input = dropdown.querySelector('input[type="text"]') as HTMLInputElement;
expect(input).toBeInTheDocument();
await userEvent.type(input, "https://github.com/kubernetes/kubernetes");
expect(searchGitReposSpy).toHaveBeenLastCalledWith(
"kubernetes/kubernetes",
3,
);
const searchedRepo = screen.getByText(MOCK_SEARCH_REPOS[0].full_name);
expect(searchedRepo).toBeInTheDocument();
await userEvent.click(searchedRepo);
expect(mockOnRepoSelection).toHaveBeenCalledWith(MOCK_SEARCH_REPOS[0]);
});
});

View File

@@ -73,7 +73,7 @@ describe("TaskCard", () => {
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
retrieveUserGitRepositoriesSpy.mockResolvedValue({ data: MOCK_RESPOSITORIES, nextPage: null });
});
it("should call create conversation with suggest task trigger and selected suggested task", async () => {

View File

@@ -1,16 +1,18 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { act } from "react";
import { MaintenanceBanner } from "#/components/features/maintenance/maintenance-banner";
// Mock react-i18next
vi.mock("react-i18next", async () => {
const actual = await vi.importActual<typeof import("react-i18next")>("react-i18next");
const actual =
await vi.importActual<typeof import("react-i18next")>("react-i18next");
return {
...actual,
useTranslation: () => ({
t: (key: string, options?: { time?: string }) => {
const translations: Record<string, string> = {
"MAINTENANCE$SCHEDULED_MESSAGE": `Scheduled maintenance will begin at ${options?.time || "{{time}}"}`,
MAINTENANCE$SCHEDULED_MESSAGE: `Scheduled maintenance will begin at ${options?.time || "{{time}}"}`,
};
return translations[key] || key;
},
@@ -19,34 +21,91 @@ vi.mock("react-i18next", async () => {
});
describe("MaintenanceBanner", () => {
afterEach(() => {
localStorage.clear();
});
it("renders maintenance banner with formatted time", () => {
const startTime = "2024-01-15T10:00:00-05:00"; // EST timestamp
const { container } = render(<MaintenanceBanner startTime={startTime} />);
// Check if the banner is rendered
expect(screen.getByText(/Scheduled maintenance will begin at/)).toBeInTheDocument();
const banner = screen.queryByTestId("maintenance-banner");
expect(banner).toBeInTheDocument();
// Check if the warning icon (SVG) is present
const svgIcon = container.querySelector('svg');
const svgIcon = container.querySelector("svg");
expect(svgIcon).toBeInTheDocument();
// Check if the button to close is present
const button = within(banner!).queryByTestId("dismiss-button");
expect(button).toBeInTheDocument();
});
// maintenance-banner
it("handles invalid date gracefully", () => {
const invalidTime = "invalid-date";
render(<MaintenanceBanner startTime={invalidTime} />);
// Should still render the banner with the original string
expect(screen.getByText(/Scheduled maintenance will begin at invalid-date/)).toBeInTheDocument();
// Check if the banner is rendered
const banner = screen.queryByTestId("maintenance-banner");
expect(banner).not.toBeInTheDocument();
});
it("formats ISO date string correctly", () => {
const isoTime = "2024-01-15T15:30:00.000Z";
it("click on dismiss button removes banner", () => {
const startTime = "2024-01-15T10:00:00-05:00"; // EST timestamp
render(<MaintenanceBanner startTime={isoTime} />);
render(<MaintenanceBanner startTime={startTime} />);
// Should render the banner (exact time format will depend on user's timezone)
expect(screen.getByText(/Scheduled maintenance will begin at/)).toBeInTheDocument();
// Check if the banner is rendered
const banner = screen.queryByTestId("maintenance-banner");
const button = within(banner!).queryByTestId("dismiss-button");
act(() => {
fireEvent.click(button!);
});
expect(banner).not.toBeInTheDocument();
});
it("banner reappears after dismissing on next maintenance event(future time)", () => {
const startTime = "2024-01-15T10:00:00-05:00"; // EST timestamp
const nextStartTime = "2025-01-15T10:00:00-05:00"; // EST timestamp
const { rerender } = render(<MaintenanceBanner startTime={startTime} />);
// Check if the banner is rendered
const banner = screen.queryByTestId("maintenance-banner");
const button = within(banner!).queryByTestId("dismiss-button");
act(() => {
fireEvent.click(button!);
});
expect(banner).not.toBeInTheDocument();
rerender(<MaintenanceBanner startTime={nextStartTime} />);
expect(screen.queryByTestId("maintenance-banner")).toBeInTheDocument();
});
it("banner doesn't reappear after dismissing on next maintenance event(past time)", () => {
const startTime = "2024-01-15T10:00:00-05:00"; // EST timestamp
const nextStartTime = "2023-01-15T10:00:00-05:00"; // EST timestamp
const { rerender } = render(<MaintenanceBanner startTime={startTime} />);
// Check if the banner is rendered
const banner = screen.queryByTestId("maintenance-banner");
const button = within(banner!).queryByTestId("dismiss-button");
act(() => {
fireEvent.click(button!);
});
expect(banner).not.toBeInTheDocument();
rerender(<MaintenanceBanner startTime={nextStartTime} />);
expect(screen.queryByTestId("maintenance-banner")).not.toBeInTheDocument();
});
});

View File

@@ -12,6 +12,23 @@ import { GitRepository } from "#/types/git";
import { RepositoryMicroagent } from "#/types/microagent-management";
import { Conversation } from "#/api/open-hands.types";
// Mock hooks
const mockUseUserProviders = vi.fn();
const mockUseUserRepositories = vi.fn();
const mockUseConfig = vi.fn();
vi.mock("#/hooks/use-user-providers", () => ({
useUserProviders: () => mockUseUserProviders(),
}));
vi.mock("#/hooks/query/use-user-repositories", () => ({
useUserRepositories: () => mockUseUserRepositories(),
}));
vi.mock("#/hooks/query/use-config", () => ({
useConfig: () => mockUseConfig(),
}));
describe("MicroagentManagement", () => {
const RouterStub = createRoutesStub([
{
@@ -151,10 +168,39 @@ describe("MicroagentManagement", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
// Setup default hook mocks
mockUseUserProviders.mockReturnValue({
providers: ["github"],
});
mockUseUserRepositories.mockReturnValue({
data: {
pages: [
{
data: mockRepositories,
nextPage: null,
},
],
},
isLoading: false,
isError: false,
hasNextPage: false,
isFetchingNextPage: false,
onLoadMore: vi.fn(),
});
mockUseConfig.mockReturnValue({
data: {
APP_MODE: "oss",
},
});
// Setup default mock for retrieveUserGitRepositories
vi.spyOn(OpenHands, "retrieveUserGitRepositories").mockResolvedValue([
...mockRepositories,
]);
vi.spyOn(OpenHands, "retrieveUserGitRepositories").mockResolvedValue({
data: [...mockRepositories],
nextPage: null,
});
// Setup default mock for getRepositoryMicroagents
vi.spyOn(OpenHands, "getRepositoryMicroagents").mockResolvedValue([
...mockMicroagents,
@@ -180,13 +226,15 @@ describe("MicroagentManagement", () => {
});
it("should display loading state when fetching repositories", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockImplementation(
() => new Promise(() => {}), // Never resolves
);
// Mock loading state
mockUseUserRepositories.mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
hasNextPage: false,
isFetchingNextPage: false,
onLoadMore: vi.fn(),
});
renderMicroagentManagement();
@@ -196,19 +244,21 @@ describe("MicroagentManagement", () => {
});
it("should handle error when fetching repositories", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockRejectedValue(
new Error("Failed to fetch repositories"),
);
// Mock error state
mockUseUserRepositories.mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
hasNextPage: false,
isFetchingNextPage: false,
onLoadMore: vi.fn(),
});
renderMicroagentManagement();
// Wait for the error to be handled
await waitFor(() => {
expect(retrieveUserGitRepositoriesSpy).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
});
@@ -217,7 +267,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Check that tabs are rendered
@@ -235,7 +285,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded and rendered
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Check that repository names are displayed
@@ -250,7 +300,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -287,7 +337,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -312,7 +362,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -337,7 +387,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -360,7 +410,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -399,7 +449,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Check that add microagent buttons are present
@@ -413,7 +463,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click the first add microagent button
@@ -432,7 +482,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click the first add microagent button
@@ -452,17 +502,28 @@ describe("MicroagentManagement", () => {
});
it("should display empty state when no repositories are found", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue([]);
// Mock empty repositories
mockUseUserRepositories.mockReturnValue({
data: {
pages: [
{
data: [],
nextPage: null,
},
],
},
isLoading: false,
isError: false,
hasNextPage: false,
isFetchingNextPage: false,
onLoadMore: vi.fn(),
});
renderMicroagentManagement();
// Wait for repositories to be loaded
await waitFor(() => {
expect(retrieveUserGitRepositoriesSpy).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Check that empty state messages are displayed
@@ -479,7 +540,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -520,7 +581,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Check that search input is rendered
@@ -540,7 +601,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Initially only repositories with .openhands should be visible
@@ -571,7 +632,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Type in search input with uppercase
@@ -594,7 +655,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Type in search input with partial match
@@ -620,7 +681,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Type in search input
@@ -653,7 +714,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Type in search input with non-existent repository name
@@ -681,7 +742,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Type in search input with special characters
@@ -702,7 +763,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Filter to show only repo2
@@ -737,7 +798,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Type in search input with leading/trailing whitespace
@@ -757,7 +818,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
const searchInput = screen.getByRole("textbox", {
@@ -789,7 +850,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -816,7 +877,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -862,7 +923,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -879,7 +940,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -904,7 +965,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -953,7 +1014,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -989,7 +1050,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -1031,7 +1092,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -1068,7 +1129,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -1112,7 +1173,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -1142,7 +1203,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -1165,7 +1226,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -1192,7 +1253,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion
@@ -1233,7 +1294,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Check that add microagent buttons are present
@@ -1247,7 +1308,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click the first add microagent button
@@ -1302,7 +1363,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click the first add microagent button
@@ -1326,7 +1387,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click the first add microagent button
@@ -1349,7 +1410,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click the first add microagent button
@@ -1382,7 +1443,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click the first add microagent button
@@ -1409,7 +1470,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click the first add microagent button
@@ -1435,7 +1496,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click the first add microagent button
@@ -1514,8 +1575,8 @@ describe("MicroagentManagement", () => {
pr_number: null,
};
const renderMicroagentManagementMain = (selectedMicroagentItem: any) => {
return renderWithProviders(<MicroagentManagementMain />, {
const renderMicroagentManagementMain = (selectedMicroagentItem: any) =>
renderWithProviders(<MicroagentManagementMain />, {
preloadedState: {
metrics: {
cost: null,
@@ -1541,7 +1602,6 @@ describe("MicroagentManagement", () => {
},
},
});
};
it("should render MicroagentManagementDefault when no microagent or conversation is selected", async () => {
renderMicroagentManagementMain(null);
@@ -2295,7 +2355,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories to be loaded
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
// Find and click on the first repository accordion to expand it
@@ -2337,7 +2397,7 @@ describe("MicroagentManagement", () => {
// Wait for repositories and expand accordion
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
const repoAccordion = screen.getByTestId("repository-name-tooltip");
@@ -2390,7 +2450,7 @@ describe("MicroagentManagement", () => {
renderMicroagentManagement();
await waitFor(() => {
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
expect(mockUseUserRepositories).toHaveBeenCalled();
});
const repoAccordion = screen.getByTestId("repository-name-tooltip");

View File

@@ -5,16 +5,32 @@ import { UserActions } from "#/components/features/sidebar/user-actions";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactElement } from "react";
// Create a mock for useIsAuthed that we can control per test
// Create mocks for all the hooks we need
const useIsAuthedMock = vi
.fn()
.mockReturnValue({ data: true, isLoading: false });
// Mock the useIsAuthed hook
const useConfigMock = vi
.fn()
.mockReturnValue({ data: { APP_MODE: "saas" }, isLoading: false });
const useUserProvidersMock = vi
.fn()
.mockReturnValue({ providers: [{ id: "github", name: "GitHub" }] });
// Mock the hooks
vi.mock("#/hooks/query/use-is-authed", () => ({
useIsAuthed: () => useIsAuthedMock(),
}));
vi.mock("#/hooks/query/use-config", () => ({
useConfig: () => useConfigMock(),
}));
vi.mock("#/hooks/use-user-providers", () => ({
useUserProviders: () => useUserProvidersMock(),
}));
describe("UserActions", () => {
const user = userEvent.setup();
const onClickAccountSettingsMock = vi.fn();
@@ -40,8 +56,10 @@ describe("UserActions", () => {
};
beforeEach(() => {
// Reset the mock to default value before each test
// Reset all mocks to default values before each test
useIsAuthedMock.mockReturnValue({ data: true, isLoading: false });
useConfigMock.mockReturnValue({ data: { APP_MODE: "saas" }, isLoading: false });
useUserProvidersMock.mockReturnValue({ providers: [{ id: "github", name: "GitHub" }] });
});
afterEach(() => {
@@ -102,6 +120,9 @@ describe("UserActions", () => {
it("should NOT show context menu when user is not authenticated and avatar is clicked", async () => {
// Set isAuthed to false for this test
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
// Keep other mocks with default values
useConfigMock.mockReturnValue({ data: { APP_MODE: "saas" }, isLoading: false });
useUserProvidersMock.mockReturnValue({ providers: [{ id: "github", name: "GitHub" }] });
renderWithQueryClient(<UserActions onLogout={onLogoutMock} />);
@@ -131,6 +152,9 @@ describe("UserActions", () => {
it("should NOT be able to access logout when user is not authenticated", async () => {
// Set isAuthed to false for this test
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
// Keep other mocks with default values
useConfigMock.mockReturnValue({ data: { APP_MODE: "saas" }, isLoading: false });
useUserProvidersMock.mockReturnValue({ providers: [{ id: "github", name: "GitHub" }] });
renderWithQueryClient(<UserActions onLogout={onLogoutMock} />);
@@ -149,6 +173,9 @@ describe("UserActions", () => {
it("should handle user prop changing from undefined to defined", async () => {
// Start with no authentication
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
// Keep other mocks with default values
useConfigMock.mockReturnValue({ data: { APP_MODE: "saas" }, isLoading: false });
useUserProvidersMock.mockReturnValue({ providers: [{ id: "github", name: "GitHub" }] });
const { rerender } = renderWithQueryClient(
<UserActions onLogout={onLogoutMock} />,
@@ -163,6 +190,9 @@ describe("UserActions", () => {
// Set authentication to true for the rerender
useIsAuthedMock.mockReturnValue({ data: true, isLoading: false });
// Ensure config and providers are set correctly
useConfigMock.mockReturnValue({ data: { APP_MODE: "saas" }, isLoading: false });
useUserProvidersMock.mockReturnValue({ providers: [{ id: "github", name: "GitHub" }] });
// Add user prop and create a new QueryClient to ensure fresh state
const queryClient = new QueryClient({
@@ -195,6 +225,11 @@ describe("UserActions", () => {
});
it("should handle user prop changing from defined to undefined", async () => {
// Start with authentication and providers
useIsAuthedMock.mockReturnValue({ data: true, isLoading: false });
useConfigMock.mockReturnValue({ data: { APP_MODE: "saas" }, isLoading: false });
useUserProvidersMock.mockReturnValue({ providers: [{ id: "github", name: "GitHub" }] });
const { rerender } = renderWithQueryClient(
<UserActions
onLogout={onLogoutMock}
@@ -211,6 +246,9 @@ describe("UserActions", () => {
// Set authentication to false for the rerender
useIsAuthedMock.mockReturnValue({ data: false, isLoading: false });
// Keep other mocks with default values
useConfigMock.mockReturnValue({ data: { APP_MODE: "saas" }, isLoading: false });
useUserProvidersMock.mockReturnValue({ providers: [{ id: "github", name: "GitHub" }] });
// Remove user prop - menu should disappear because user is no longer authenticated
rerender(
@@ -229,6 +267,11 @@ describe("UserActions", () => {
});
it("should work with loading state and user provided", async () => {
// Ensure authentication and providers are set correctly
useIsAuthedMock.mockReturnValue({ data: true, isLoading: false });
useConfigMock.mockReturnValue({ data: { APP_MODE: "saas" }, isLoading: false });
useUserProvidersMock.mockReturnValue({ providers: [{ id: "github", name: "GitHub" }] });
renderWithQueryClient(
<UserActions
onLogout={onLogoutMock}

View File

@@ -0,0 +1,135 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook } from "@testing-library/react";
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
// Mock the useActiveConversation hook
vi.mock("#/hooks/query/use-active-conversation");
const mockUseActiveConversation = vi.mocked(useActiveConversation);
describe("useDocumentTitleFromState", () => {
const originalTitle = document.title;
beforeEach(() => {
vi.clearAllMocks();
document.title = "Test";
});
afterEach(() => {
document.title = originalTitle;
vi.resetAllMocks();
});
it("should set document title to default suffix when no conversation", () => {
mockUseActiveConversation.mockReturnValue({
data: null,
} as any);
renderHook(() => useDocumentTitleFromState());
expect(document.title).toBe("OpenHands");
});
it("should set document title to custom suffix when no conversation", () => {
mockUseActiveConversation.mockReturnValue({
data: null,
} as any);
renderHook(() => useDocumentTitleFromState("Custom App"));
expect(document.title).toBe("Custom App");
});
it("should set document title with conversation title", () => {
mockUseActiveConversation.mockReturnValue({
data: {
conversation_id: "123",
title: "My Conversation",
status: "RUNNING",
},
} as any);
renderHook(() => useDocumentTitleFromState());
expect(document.title).toBe("My Conversation | OpenHands");
});
it("should update document title when conversation title changes", () => {
// Initial state - no conversation
mockUseActiveConversation.mockReturnValue({
data: null,
} as any);
const { rerender } = renderHook(() => useDocumentTitleFromState());
expect(document.title).toBe("OpenHands");
// Conversation with initial title
mockUseActiveConversation.mockReturnValue({
data: {
conversation_id: "123",
title: "Conversation 65e29",
status: "RUNNING",
},
} as any);
rerender();
expect(document.title).toBe("Conversation 65e29 | OpenHands");
// Conversation title updated to human-readable title
mockUseActiveConversation.mockReturnValue({
data: {
conversation_id: "123",
title: "Help me build a React app",
status: "RUNNING",
},
} as any);
rerender();
expect(document.title).toBe("Help me build a React app | OpenHands");
});
it("should handle conversation without title", () => {
mockUseActiveConversation.mockReturnValue({
data: {
conversation_id: "123",
title: undefined,
status: "RUNNING",
},
} as any);
renderHook(() => useDocumentTitleFromState());
expect(document.title).toBe("OpenHands");
});
it("should handle empty conversation title", () => {
mockUseActiveConversation.mockReturnValue({
data: {
conversation_id: "123",
title: "",
status: "RUNNING",
},
} as any);
renderHook(() => useDocumentTitleFromState());
expect(document.title).toBe("OpenHands");
});
it("should reset document title on cleanup", () => {
mockUseActiveConversation.mockReturnValue({
data: {
conversation_id: "123",
title: "My Conversation",
status: "RUNNING",
},
} as any);
const { unmount } = renderHook(() => useDocumentTitleFromState());
expect(document.title).toBe("My Conversation | OpenHands");
unmount();
expect(document.title).toBe("OpenHands");
});
});

View File

@@ -3,6 +3,8 @@ import { createRoutesStub } from "react-router";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import userEvent from "@testing-library/user-event";
import i18next from "i18next";
import { I18nextProvider } from "react-i18next";
import GitSettingsScreen from "#/routes/git-settings";
import OpenHands from "#/api/open-hands";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
@@ -46,22 +48,44 @@ const GitSettingsRouterStub = createRoutesStub([
]);
const renderGitSettingsScreen = () => {
// Initialize i18next instance
i18next.init({
lng: "en",
resources: {
en: {
translation: {
GITHUB$TOKEN_HELP_TEXT: "Help text",
GITHUB$TOKEN_LABEL: "GitHub Token",
GITHUB$HOST_LABEL: "GitHub Host",
GITLAB$TOKEN_LABEL: "GitLab Token",
GITLAB$HOST_LABEL: "GitLab Host",
BITBUCKET$TOKEN_LABEL: "Bitbucket Token",
BITBUCKET$HOST_LABEL: "Bitbucket Host",
},
},
},
});
const { rerender, ...rest } = render(
<GitSettingsRouterStub initialEntries={["/settings/integrations"]} />,
{
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
<I18nextProvider i18n={i18next}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</I18nextProvider>
),
},
);
const rerenderGitSettingsScreen = () =>
rerender(
<QueryClientProvider client={queryClient}>
<GitSettingsRouterStub initialEntries={["/settings/integrations"]} />
</QueryClientProvider>,
<I18nextProvider i18n={i18next}>
<QueryClientProvider client={queryClient}>
<GitSettingsRouterStub initialEntries={["/settings/integrations"]} />
</QueryClientProvider>
</I18nextProvider>,
);
return {
@@ -351,14 +375,18 @@ describe("Form submission", () => {
let disconnectButton = await screen.findByTestId(
"disconnect-tokens-button",
);
// When tokens are set (github and gitlab are not null), the button should be enabled
await waitFor(() => expect(disconnectButton).not.toBeDisabled());
// Mock settings with no tokens set
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {},
});
queryClient.invalidateQueries();
disconnectButton = await screen.findByTestId("disconnect-tokens-button");
// When no tokens are set, the button should be disabled
await waitFor(() => expect(disconnectButton).toBeDisabled());
});

View File

@@ -32,6 +32,42 @@ const RouterStub = createRoutesStub([
},
]);
const selectRepository = async (repoName: string) => {
const repoConnector = screen.getByTestId("repo-connector");
// First select the provider
const providerDropdown = await waitFor(() =>
screen.getByText("Select Provider"),
);
await userEvent.click(providerDropdown);
await userEvent.click(screen.getByText("Github"));
// Then select the repository
const dropdown = within(repoConnector).getByTestId("repo-dropdown");
const repoInput = within(dropdown).getByRole("combobox");
await userEvent.click(repoInput);
// Wait for the options to be loaded and displayed
await waitFor(() => {
const options = screen.getAllByText(repoName);
// Find the option in the dropdown (it will have role="option")
const dropdownOption = options.find(
(el) => el.getAttribute("role") === "option",
);
expect(dropdownOption).toBeInTheDocument();
});
const options = screen.getAllByText(repoName);
const dropdownOption = options.find(
(el) => el.getAttribute("role") === "option",
);
await userEvent.click(dropdownOption!);
// Wait for the branch to be auto-selected
await waitFor(() => {
expect(screen.getByText("main")).toBeInTheDocument();
});
};
const renderHomeScreen = () =>
render(<RouterStub />, {
wrapper: ({ children }) => (
@@ -93,84 +129,8 @@ describe("HomeScreen", () => {
expect(mainContainer).toHaveClass("flex", "flex-col", "lg:flex-row");
});
it("should filter the suggested tasks based on the selected repository", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
renderHomeScreen();
const taskSuggestions = await screen.findByTestId("task-suggestions");
// Initially, all tasks should be visible
await waitFor(() => {
within(taskSuggestions).getByText("octocat/hello-world");
within(taskSuggestions).getByText("octocat/earth");
});
// Select a repository from the dropdown
const repoConnector = screen.getByTestId("repo-connector");
const dropdown = within(repoConnector).getByTestId("repo-dropdown");
await userEvent.click(dropdown);
const repoOption = screen.getAllByText("octocat/hello-world")[1];
await userEvent.click(repoOption);
// After selecting a repository, only tasks related to that repository should be visible
await waitFor(() => {
within(taskSuggestions).getByText("octocat/hello-world");
expect(
within(taskSuggestions).queryByText("octocat/earth"),
).not.toBeInTheDocument();
});
});
it("should reset the filtered tasks when the selected repository is cleared", async () => {
const retrieveUserGitRepositoriesSpy = vi.spyOn(
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
renderHomeScreen();
const taskSuggestions = await screen.findByTestId("task-suggestions");
// Initially, all tasks should be visible
await waitFor(() => {
within(taskSuggestions).getByText("octocat/hello-world");
within(taskSuggestions).getByText("octocat/earth");
});
// Select a repository from the dropdown
const repoConnector = screen.getByTestId("repo-connector");
const dropdown = within(repoConnector).getByTestId("repo-dropdown");
await userEvent.click(dropdown);
const repoOption = screen.getAllByText("octocat/hello-world")[1];
await userEvent.click(repoOption);
// After selecting a repository, only tasks related to that repository should be visible
await waitFor(() => {
within(taskSuggestions).getByText("octocat/hello-world");
expect(
within(taskSuggestions).queryByText("octocat/earth"),
).not.toBeInTheDocument();
});
// Clear the selected repository
await userEvent.clear(dropdown);
// All tasks should be visible again
await waitFor(() => {
within(taskSuggestions).getByText("octocat/hello-world");
within(taskSuggestions).getByText("octocat/earth");
});
});
// TODO: Fix this test
it.skip("should filter and reset the suggested tasks based on repository selection", async () => {});
describe("launch buttons", () => {
const setupLaunchButtons = async () => {
@@ -179,19 +139,25 @@ describe("HomeScreen", () => {
let tasksLaunchButtons =
await screen.findAllByTestId("task-launch-button");
// Select a repository from the dropdown to enable the repo launch button
const repoConnector = screen.getByTestId("repo-connector");
const dropdown = within(repoConnector).getByTestId("repo-dropdown");
await userEvent.click(dropdown);
const repoOption = screen.getAllByText("octocat/hello-world")[1];
await userEvent.click(repoOption);
// Mock the repository branches API call
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue([
{ name: "main", commit_sha: "123", protected: false },
{ name: "develop", commit_sha: "456", protected: false },
]);
expect(headerLaunchButton).not.toBeDisabled();
expect(repoLaunchButton).not.toBeDisabled();
tasksLaunchButtons.forEach((button) => {
expect(button).not.toBeDisabled();
// Select a repository to enable the repo launch button
await selectRepository("octocat/hello-world");
// Wait for all buttons to be enabled
await waitFor(() => {
expect(headerLaunchButton).not.toBeDisabled();
expect(repoLaunchButton).not.toBeDisabled();
tasksLaunchButtons.forEach((button) => {
expect(button).not.toBeDisabled();
});
});
// Get fresh references to the buttons
headerLaunchButton = screen.getByTestId("header-launch-button");
repoLaunchButton = screen.getByTestId("repo-launch-button");
tasksLaunchButtons = await screen.findAllByTestId("task-launch-button");
@@ -208,7 +174,10 @@ describe("HomeScreen", () => {
OpenHands,
"retrieveUserGitRepositories",
);
retrieveUserGitRepositoriesSpy.mockResolvedValue(MOCK_RESPOSITORIES);
retrieveUserGitRepositoriesSpy.mockResolvedValue({
data: MOCK_RESPOSITORIES,
nextPage: null,
});
});
it("should disable the other launch buttons when the header launch button is clicked", async () => {

View File

@@ -47,7 +47,7 @@ describe("Content", () => {
const apiKey = screen.getByTestId("llm-api-key-input");
await waitFor(() => {
expect(provider).toHaveValue("Anthropic");
expect(provider).toHaveValue("OpenHands");
expect(model).toHaveValue("claude-sonnet-4-20250514");
expect(apiKey).toHaveValue("");
@@ -135,7 +135,7 @@ describe("Content", () => {
);
const condensor = screen.getByTestId("enable-memory-condenser-switch");
expect(model).toHaveValue("anthropic/claude-sonnet-4-20250514");
expect(model).toHaveValue("openhands/claude-sonnet-4-20250514");
expect(baseUrl).toHaveValue("");
expect(apiKey).toHaveValue("");
expect(apiKey).toHaveProperty("placeholder", "");
@@ -537,7 +537,7 @@ describe("Form submission", () => {
// select provider
await userEvent.click(provider);
const providerOption = screen.getByText("Anthropic");
const providerOption = screen.getByText("OpenHands");
await userEvent.click(providerOption);
// select model
@@ -550,7 +550,7 @@ describe("Form submission", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_model: "anthropic/claude-sonnet-4-20250514",
llm_model: "openhands/claude-sonnet-4-20250514",
llm_base_url: "",
confirmation_mode: false,
}),

View File

@@ -112,12 +112,21 @@ describe("Content", () => {
screen.getByTestId("git-settings-screen");
});
it("should render a message if there are no existing secrets", async () => {
it("should render an empty table when there are no existing secrets", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
getSecretsSpy.mockResolvedValue([]);
renderSecretsSettings();
await screen.findByTestId("no-secrets-message");
// Should show the add secret button
await screen.findByTestId("add-secret-button");
// Should show an empty table with headers but no secret items
expect(screen.queryAllByTestId("secret-item")).toHaveLength(0);
// Should still show the table headers
expect(screen.getByText("SETTINGS$NAME")).toBeInTheDocument();
expect(screen.getByText("SECRETS$DESCRIPTION")).toBeInTheDocument();
expect(screen.getByText("SETTINGS$ACTIONS")).toBeInTheDocument();
});
it("should render existing secrets", async () => {
@@ -127,7 +136,6 @@ describe("Content", () => {
const secrets = await screen.findAllByTestId("secret-item");
expect(secrets).toHaveLength(2);
expect(screen.queryByTestId("no-secrets-message")).not.toBeInTheDocument();
});
});
@@ -399,19 +407,22 @@ describe("Secret actions", () => {
expect(screen.queryByText("My_Secret_2")).toBeInTheDocument();
});
it("should hide the no items message when in form view", async () => {
it("should hide the table and add button when in form view", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
getSecretsSpy.mockResolvedValue([]);
renderSecretsSettings();
// render form & hide items
expect(screen.queryByTestId("no-secrets-message")).not.toBeInTheDocument();
// Initially should show the add button and table
const button = await screen.findByTestId("add-secret-button");
expect(screen.getByText("SETTINGS$NAME")).toBeInTheDocument(); // table header
await userEvent.click(button);
// When in form view, should hide the add button and table
const secretForm = screen.getByTestId("add-secret-form");
expect(secretForm).toBeInTheDocument();
expect(screen.queryByTestId("no-secrets-message")).not.toBeInTheDocument();
expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument();
expect(screen.queryByText("SETTINGS$NAME")).not.toBeInTheDocument(); // table header should be hidden
});
it("should not allow spaces in secret names", async () => {

View File

@@ -82,5 +82,11 @@ describe("extractModelAndProvider", () => {
model: "claude-opus-4-20250514",
separator: "/",
});
expect(extractModelAndProvider("claude-opus-4-1-20250805")).toEqual({
provider: "anthropic",
model: "claude-opus-4-1-20250805",
separator: "/",
});
});
});

View File

@@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="OpenHands: Code Less, Make More"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>OpenHands</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.51.1",
"version": "0.52.1",
"private": true,
"type": "module",
"engines": {
@@ -8,22 +8,25 @@
},
"dependencies": {
"@heroui/react": "^2.8.2",
"@heroui/use-infinite-scroll": "^2.2.10",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.7.1",
"@react-router/serve": "^7.7.1",
"@react-router/node": "^7.8.0",
"@react-router/serve": "^7.8.0",
"@react-types/shared": "^3.31.0",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.8.1",
"@stripe/stripe-js": "^7.7.0",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.8.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.84.1",
"@vitejs/plugin-react": "^4.7.0",
"@tanstack/react-query": "^5.84.2",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^5.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.11.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.12",
"i18next": "^25.3.2",
@@ -31,9 +34,9 @@
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.29",
"jose": "^6.0.12",
"lucide-react": "^0.536.0",
"lucide-react": "^0.539.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.258.5",
"posthog-js": "^1.259.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
@@ -42,7 +45,8 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.7.1",
"react-router": "^7.8.0",
"react-select": "^5.10.2",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"remark-breaks": "^4.0.0",
@@ -50,7 +54,7 @@
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"vite": "^7.0.6",
"vite": "^7.1.1",
"web-vitals": "^5.1.0",
"ws": "^8.18.2"
},
@@ -85,14 +89,14 @@
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.54.2",
"@react-router/dev": "^7.7.1",
"@react-router/dev": "^7.8.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.1.0",
"@types/node": "^24.2.0",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@types/react-highlight": "^0.12.8",
@@ -110,13 +114,13 @@
"eslint-plugin-i18next": "^6.1.3",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.1.7",
"jsdom": "^26.1.0",
"lint-staged": "^16.1.2",
"lint-staged": "^16.1.4",
"msw": "^2.6.6",
"prettier": "^3.6.2",
"stripe": "^18.4.0",

View File

@@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.10.3'
const PACKAGE_VERSION = '2.10.4'
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View File

@@ -1,66 +0,0 @@
import { openHands } from "../open-hands-axios";
import { GetFilesResponse, GetFileResponse } from "./file-service.types";
import { getConversationUrl } from "../conversation.utils";
import { FileUploadSuccessResponse } from "../open-hands.types";
export class FileService {
/**
* Retrieve the list of files available in the workspace
* @param conversationId ID of the conversation
* @param path Path to list files from. If provided, it lists all the files in the given path
* @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace
*/
static async getFiles(
conversationId: string,
path?: string,
): Promise<GetFilesResponse> {
const url = `${getConversationUrl(conversationId)}/list-files`;
const { data } = await openHands.get<GetFilesResponse>(url, {
params: { path },
});
return data;
}
/**
* Retrieve the content of a file
* @param conversationId ID of the conversation
* @param path Full path of the file to retrieve
* @returns Code content of the file
*/
static async getFile(conversationId: string, path: string): Promise<string> {
const url = `${getConversationUrl(conversationId)}/select-file`;
const { data } = await openHands.get<GetFileResponse>(url, {
params: { file: path },
});
return data.code;
}
/**
* Upload multiple files to the workspace
* @param conversationId ID of the conversation
* @param files List of files.
* @returns list of uploaded files, list of skipped files
*/
static async uploadFiles(
conversationId: string,
files: File[],
): Promise<FileUploadSuccessResponse> {
const formData = new FormData();
for (const file of files) {
formData.append("files", file);
}
const url = `${getConversationUrl(conversationId)}/upload-files`;
const response = await openHands.post<FileUploadSuccessResponse>(
url,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
},
);
return response.data;
}
}

View File

@@ -1,5 +0,0 @@
export type GetFilesResponse = string[];
export interface GetFileResponse {
code: string;
}

View File

@@ -1,21 +0,0 @@
import { openHands } from "../open-hands-axios";
interface GetPromptResponse {
status: string;
prompt: string;
}
export class MemoryService {
static async getPrompt(
conversationId: string,
eventId: number,
): Promise<string> {
const { data } = await openHands.get<GetPromptResponse>(
`/api/conversations/${conversationId}/remember_prompt`,
{
params: { event_id: eventId },
},
);
return data.prompt;
}
}

Some files were not shown because too many files have changed in this diff Show More