* fix(upload): resolve relative working dirs against /api/file/home
The agent-server's /api/file/upload endpoint requires an absolute path
and `mkdir -p`'s the parent. The frontend's `toAbsoluteWorkspacePath`
was naively prepending `/` to the default relative `workspace/project`
working dir, producing `/workspace/project/<hex>/...`. On macOS and
fresh Docker images that path lives under a read-only filesystem root,
so uploads failed with `OSError: [Errno 30] Read-only file system:
'/workspace'`.
Conversations themselves kept working because the agent-server resolves
relative `workspace.working_dir` against its own process CWD (which is
writable in dev), so the worktree landed elsewhere and only uploads
mistargeted the read-only root.
Fix: introduce `getAgentServerHomeDir` (cached per backend, backed by
`FileClient.getHome` → `GET /api/file/home`) and a
`resolveAbsoluteAgentServerPath` helper. Both the conversation-start
payload and the file-upload destination now go through this resolver,
so they always agree on a single absolute path anchored at the
agent-server's home directory (e.g. `~/workspace/project/<hex>`).
Absolute paths pass through unchanged, so explicit workspace selections
and `VITE_WORKING_DIR` overrides are unaffected.
Spec: WUP-001 in specs/workspace-upload-path.md.
Co-authored-by: openhands <openhands@all-hands.dev>
* docs: clarify resolveConversationUploadWorkingDir returns raw (possibly-relative) working dir
Add JSDoc explaining that callers must funnel the result through
buildWorkspaceUploadPath (which calls resolveAbsoluteWorkspacePath) to
get an absolute path for the upload endpoint. Also document that the
UNC path case is already covered by the existing isAbsolutePath regex.
Addresses review comment on PR #1106.
Co-authored-by: openhands <openhands@all-hands.dev>
* test(e2e): add mock-llm image-upload test
Adds an end-to-end mock-LLM test that exercises the full image-attachment
pipeline:
1. Attaches a minimal 1×1 PNG to the home-page chat via the hidden file
input (data-testid="upload-image-input") using Playwright's
setInputFiles.
2. Submits "What is in this image?" — creating a conversation and sending
the message via sendMessageWithAttachments.
3. Verifies the agent replies with IMAGE_REPLY_TOKEN in the chat UI.
4. Verifies the user MessageEvent in the conversation events API has
image_urls populated (base64 data: URL).
5. Verifies at least one /v1/chat/completions call to the mock server
contained an image_url content block — confirming the image was
forwarded to the LLM as expected.
Supporting changes:
- mock-llm-server.py: add GET /admin/requests endpoint that exposes all
captured completion request bodies since the last reset; reset also
clears the history.
- mock-llm-helpers.ts: add getMockLLMRequests(), IMAGE_REPLY_TOKEN,
and MINIMAL_PNG_BASE64 exports.
- AGENTS.md: document the new endpoint and test spec.
Co-authored-by: openhands <openhands@all-hands.dev>
* test(e2e): add padding response to image-upload trajectory
The agent-server makes one internal LLM call for skill-analysis before
the main agent loop starts. The original 1-response trajectory was
consumed by that internal call, leaving the agent with a 500 error and
retry storm.
Add 1 padding response (turn 0: empty text) + 1 safety buffer (turn 2)
following the same pattern as mock-llm-automation.spec.ts.
Co-authored-by: openhands <openhands@all-hands.dev>
* test(e2e): use gpt-4o model name for vision-capable LLM requests
litellm strips image_url content blocks for unknown model names like
'openai/mock-test-model'. Switch to 'openai/gpt-4o' so litellm knows
the model accepts vision content and includes base64 image_url blocks
in the completion request body.
The base_url still points at the local mock server; the model name is
only a hint to litellm's request formatter.
Also improve assertion diagnostics to print all captured LLM requests
(not just the first) when the assertion fails.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
* fix: always send frontend's chosen default model to agent-server (#807)
When the agent-server returns an empty model string (e.g. because no
settings have been saved yet), the adapter's type guard accepted it as
a valid string and forwarded it verbatim. The agent-server would then
fall back to its SDK default ('gpt-5.5') rather than using the
frontend's chosen default ('openhands/minimax-m2.7').
Fix: strengthen the guard in buildConfiguredOpenHandsAgentSettings to
also reject empty/whitespace-only strings, so the frontend's own
default is always sent explicitly:
llm.model =
typeof llm.model === 'string' && llm.model.trim().length > 0
? llm.model
: DEFAULT_SETTINGS.llm_model;
While here: align the legacy V0 /api/options/models mock endpoint's
default_model with DEFAULT_SETTINGS (was incorrectly set to claude-opus;
should be minimax-m2.7 to match the rest of the defaults), and document
the canonical default model and the two-location update rule in AGENTS.md.
Closes#807
Co-authored-by: openhands <openhands@all-hands.dev>
* refactor(tests): parameterize llm.model fallback tests + add spec + AGENTS.md
- Extract getModelFrom() helper to eliminate repeated as unknown as ModelPayload casts
- Consolidate 7 individual fallback it() blocks into two it.each() groups:
(1) agent_settings variants (undefined/empty/whitespace/no-llm-block/empty-settings)
(2) encryptedAgentSettings variants (empty model / empty object)
- Rename @spec annotation from BM-807 to LLD-001 (not a backend-management spec)
- Add specs/llm-defaults.md with LLD-001 checklist
- Add AGENTS.md note documenting canonical default model, its location, and
the two-location update rule
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
* Add backend management specs and @spec annotations
- Curate specs/backend-management.md with 4 behavioral specs (BM-001–BM-004)
- Add @spec comments to source and test files for traceability
- Add spec file convention note to AGENTS.md
Co-authored-by: openhands <openhands@all-hands.dev>
* Remove duplicate BM-002 test (non-conversation redirect)
The 'stays on settings' case was covered by two tests; keep the one
with the clearer assertion and drop the redundant copy.
Co-authored-by: openhands <openhands@all-hands.dev>
* Parameterize BM-002 tests with it.each
Collapse 3 identical-structure redirect tests into a single
parameterized it.each covering conversation detail, automation detail,
and non-ID routes.
Co-authored-by: openhands <openhands@all-hands.dev>
* Document @spec tagging convention in AGENTS.md
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>
* spec(BM-001): add spec + failing tests for auto-switch on connect
Adding a backend should automatically switch the active selection to it.
Currently addBackend registers the entry but leaves the user on the
previous backend, forcing a manual switch.
- specs/backend-management.md: BM-001 definition
- Two new test cases (cloud + local) that assert the active backend
changes after addBackend — both fail against the current implementation.
Co-authored-by: openhands <openhands@all-hands.dev>
* fix(BM-001): auto-switch active backend on addBackend
addBackend now calls setActiveSelection after registering the new entry,
so the user lands on the backend they just connected — whether via the
manual host+key form or the cloud OAuth device-flow login.
- src/contexts/active-backend-context.tsx: one-line fix in addBackend
- Updated add-backend-modal test to assert the new active selection
- Updated backend-selector tests that used addBackend purely as setup
to reset active to the default local backend afterward
Co-authored-by: openhands <openhands@all-hands.dev>
* chore: remove noisy @spec markers from test setup comments
Keep @spec BM-001 only where it marks the implementation or directly
asserts the spec behavior. Setup-only adjustments (resetting active
backend after addBackend) are incidental — plain comments suffice.
Co-authored-by: openhands <openhands@all-hands.dev>
* chore: consolidate @spec BM-001 to one test + one implementation site
The spec marker belongs in exactly two places: the line that implements
the behavior and the single test that verifies it. Removed the redundant
local-backend variant (same code path as cloud) and dropped @spec labels
from the modal test (its assertion stays, just without the tag).
Co-authored-by: openhands <openhands@all-hands.dev>
* refactor: remove setActive workarounds from backend-selector tests
Instead of manually resetting active state after addBackend, tests now
work with the auto-switch naturally:
- Local backend tests: click the seeded default 'Local' (which is no
longer active after auto-switch) to trigger the switch.
- Cloud org tests: add a local backend after the cloud one so the last
auto-switch lands on local, leaving cloud backends unselected and
their org rows visible in the dropdown.
No ctx.setActive(DEFAULT_LOCAL_BACKEND_ID) calls remain.
Co-authored-by: openhands <openhands@all-hands.dev>
* refactor: extract shared seed constants in backend-selector tests
SEED_LOCAL_1 and SEED_CLOUD_PRODUCTION replace 12+4 identical inline
config objects. The two remaining inline blocks have different apiKey
values and correctly stay as-is.
Co-authored-by: openhands <openhands@all-hands.dev>
---------
Co-authored-by: openhands <openhands@all-hands.dev>