-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add runtime size configuration feature (#5805)
Co-authored-by: openhands <[email protected]> Co-authored-by: amanape <[email protected]>
- Loading branch information
1 parent
8cfcdd7
commit 1f8a018
Showing
12 changed files
with
274 additions
and
10 deletions.
There are no files selected for viewing
35 changes: 35 additions & 0 deletions
35
frontend/__tests__/components/shared/modals/settings/runtime-size-selector.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { screen } from "@testing-library/react"; | ||
import { describe, it, expect } from "vitest"; | ||
import { renderWithProviders } from "test-utils"; | ||
import { RuntimeSizeSelector } from "#/components/shared/modals/settings/runtime-size-selector"; | ||
|
||
const renderRuntimeSizeSelector = () => | ||
renderWithProviders(<RuntimeSizeSelector isDisabled={false} />); | ||
|
||
describe("RuntimeSizeSelector", () => { | ||
it("should show both runtime size options", () => { | ||
renderRuntimeSizeSelector(); | ||
// The options are in the hidden select element | ||
const select = screen.getByRole("combobox", { hidden: true }); | ||
expect(select).toHaveValue("1"); | ||
expect(select).toHaveDisplayValue("1x (2 core, 8G)"); | ||
expect(select.children).toHaveLength(3); // Empty option + 2 size options | ||
}); | ||
|
||
it("should show the full description text for disabled options", async () => { | ||
renderRuntimeSizeSelector(); | ||
|
||
// Click the button to open the dropdown | ||
const button = screen.getByRole("button", { | ||
name: "1x (2 core, 8G) SETTINGS_FORM$RUNTIME_SIZE_LABEL", | ||
}); | ||
button.click(); | ||
|
||
// Wait for the dropdown to open and find the description text | ||
const description = await screen.findByText( | ||
"Runtime sizes over 1 are disabled by default, please contact [email protected] to get access to larger runtimes.", | ||
); | ||
expect(description).toBeInTheDocument(); | ||
expect(description).toHaveClass("whitespace-normal", "break-words"); | ||
}); | ||
}); |
45 changes: 45 additions & 0 deletions
45
frontend/__tests__/components/shared/modals/settings/settings-form.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { screen, fireEvent } from "@testing-library/react"; | ||
import { describe, it, expect, vi } from "vitest"; | ||
import { renderWithProviders } from "test-utils"; | ||
import { createRoutesStub } from "react-router"; | ||
import { DEFAULT_SETTINGS } from "#/services/settings"; | ||
import { SettingsForm } from "#/components/shared/modals/settings/settings-form"; | ||
import OpenHands from "#/api/open-hands"; | ||
|
||
describe("SettingsForm", () => { | ||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); | ||
getConfigSpy.mockResolvedValue({ | ||
APP_MODE: "saas", | ||
GITHUB_CLIENT_ID: "123", | ||
POSTHOG_CLIENT_KEY: "123", | ||
}); | ||
|
||
const RouterStub = createRoutesStub([ | ||
{ | ||
Component: () => ( | ||
<SettingsForm | ||
settings={DEFAULT_SETTINGS} | ||
models={[]} | ||
agents={[]} | ||
securityAnalyzers={[]} | ||
onClose={() => {}} | ||
/> | ||
), | ||
path: "/", | ||
}, | ||
]); | ||
|
||
it("should not show runtime size selector by default", () => { | ||
renderWithProviders(<RouterStub />); | ||
expect(screen.queryByText("Runtime Size")).not.toBeInTheDocument(); | ||
}); | ||
|
||
it("should show runtime size selector when advanced options are enabled", async () => { | ||
renderWithProviders(<RouterStub />); | ||
const advancedSwitch = screen.getByRole("switch", { | ||
name: "SETTINGS_FORM$ADVANCED_OPTIONS_LABEL", | ||
}); | ||
fireEvent.click(advancedSwitch); | ||
await screen.findByText("SETTINGS_FORM$RUNTIME_SIZE_LABEL"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
frontend/src/components/shared/modals/settings/runtime-size-selector.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { useTranslation } from "react-i18next"; | ||
import { Select, SelectItem } from "@nextui-org/react"; | ||
|
||
interface RuntimeSizeSelectorProps { | ||
isDisabled: boolean; | ||
defaultValue?: number; | ||
} | ||
|
||
export function RuntimeSizeSelector({ | ||
isDisabled, | ||
defaultValue, | ||
}: RuntimeSizeSelectorProps) { | ||
const { t } = useTranslation(); | ||
|
||
return ( | ||
<fieldset className="flex flex-col gap-2"> | ||
<label | ||
htmlFor="runtime-size" | ||
className="font-[500] text-[#A3A3A3] text-xs" | ||
> | ||
{t("SETTINGS_FORM$RUNTIME_SIZE_LABEL")} | ||
</label> | ||
<Select | ||
id="runtime-size" | ||
name="runtime-size" | ||
defaultSelectedKeys={[String(defaultValue || 1)]} | ||
isDisabled={isDisabled} | ||
aria-label={t("SETTINGS_FORM$RUNTIME_SIZE_LABEL")} | ||
classNames={{ | ||
trigger: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", | ||
}} | ||
> | ||
<SelectItem key="1" value={1}> | ||
1x (2 core, 8G) | ||
</SelectItem> | ||
<SelectItem | ||
key="2" | ||
value={2} | ||
isDisabled | ||
classNames={{ | ||
description: | ||
"whitespace-normal break-words min-w-[300px] max-w-[300px]", | ||
base: "min-w-[300px] max-w-[300px]", | ||
}} | ||
description="Runtime sizes over 1 are disabled by default, please contact [email protected] to get access to larger runtimes." | ||
> | ||
2x (4 core, 16G) | ||
</SelectItem> | ||
</Select> | ||
</fieldset> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
from unittest.mock import AsyncMock, MagicMock, patch | ||
|
||
import pytest | ||
from fastapi.testclient import TestClient | ||
|
||
from openhands.core.config.sandbox_config import SandboxConfig | ||
from openhands.server.app import app | ||
from openhands.server.settings import Settings | ||
|
||
|
||
@pytest.fixture | ||
def test_client(): | ||
# Mock the middleware that adds github_token | ||
class MockMiddleware: | ||
def __init__(self, app): | ||
self.app = app | ||
|
||
async def __call__(self, scope, receive, send): | ||
if scope['type'] == 'http': | ||
scope['state'] = {'github_token': 'test-token'} | ||
await self.app(scope, receive, send) | ||
|
||
# Replace the middleware | ||
app.middleware_stack = None # Clear existing middleware | ||
app.add_middleware(MockMiddleware) | ||
|
||
return TestClient(app) | ||
|
||
|
||
@pytest.fixture | ||
def mock_settings_store(): | ||
with patch('openhands.server.routes.settings.SettingsStoreImpl') as mock: | ||
store_instance = MagicMock() | ||
mock.get_instance = AsyncMock(return_value=store_instance) | ||
store_instance.load = AsyncMock() | ||
store_instance.store = AsyncMock() | ||
yield store_instance | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_settings_api_runtime_factor(test_client, mock_settings_store): | ||
# Mock the settings store to return None initially (no existing settings) | ||
mock_settings_store.load.return_value = None | ||
|
||
# Test data with remote_runtime_resource_factor | ||
settings_data = { | ||
'language': 'en', | ||
'agent': 'test-agent', | ||
'max_iterations': 100, | ||
'security_analyzer': 'default', | ||
'confirmation_mode': True, | ||
'llm_model': 'test-model', | ||
'llm_api_key': None, | ||
'llm_base_url': 'https://test.com', | ||
'remote_runtime_resource_factor': 2, | ||
} | ||
|
||
# The test_client fixture already handles authentication | ||
|
||
# Make the POST request to store settings | ||
response = test_client.post('/api/settings', json=settings_data) | ||
assert response.status_code == 200 | ||
|
||
# Verify the settings were stored with the correct runtime factor | ||
stored_settings = mock_settings_store.store.call_args[0][0] | ||
assert stored_settings.remote_runtime_resource_factor == 2 | ||
|
||
# Mock settings store to return our settings for the GET request | ||
mock_settings_store.load.return_value = Settings(**settings_data) | ||
|
||
# Make a GET request to retrieve settings | ||
response = test_client.get('/api/settings') | ||
assert response.status_code == 200 | ||
assert response.json()['remote_runtime_resource_factor'] == 2 | ||
|
||
# Verify that the sandbox config gets updated when settings are loaded | ||
with patch('openhands.server.shared.config') as mock_config: | ||
mock_config.sandbox = SandboxConfig() | ||
response = test_client.get('/api/settings') | ||
assert response.status_code == 200 | ||
|
||
# Verify that the sandbox config was updated with the new value | ||
mock_settings_store.store.assert_called() | ||
stored_settings = mock_settings_store.store.call_args[0][0] | ||
assert stored_settings.remote_runtime_resource_factor == 2 |