Files
OpenHands/__tests__/api/option-service.test.ts
T
6f25fb6630 fix(onboarding): unblock keyless/open local backends in the connect flow (#1133)
* fix(onboarding): unblock keyless/open local backends in the connect flow

Three pre-existing bugs blocked onboarding an open (keyless) local
agent-server, e.g. the examples/acp-docker backend:

- check-backend-step: "Next" was gated on a non-empty API key
  (requireApiKey was always on) even though the health probe already
  showed the backend connected without one. Gate it on isAuthRequired()
  so keyless local backends pass through; cloud still requires a key via
  kind !== "local".

- root.tsx: the no-backend gate rendered ManageBackendsModal with a
  no-op onClose, so "Done" did nothing and the app never advanced after
  the first backend was added. Re-run the /server_info probe on close
  once a usable local backend exists.

- agent-server-compatibility: the empty-registry case reused the
  "could not connect / start a compatible server" copy. Split it so no
  backend says "add a backend" and a real probe failure says "make sure
  it's reachable".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(test): update expected error message in option-service test

The PR changed AgentServerUnavailableError's message from
"Agent server not found..." to "Could not connect to the configured
agent server...". Update the test to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:04:53 +02:00

131 lines
4.1 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { http, HttpResponse } from "msw";
import {
AgentServerUnavailableError,
clearCachedAgentServerInfo,
isAgentServerToolAvailable,
} from "#/api/agent-server-compatibility";
import OptionService from "#/api/option-service/option-service.api";
import { server } from "#/mocks/node";
describe("OptionService", () => {
beforeEach(() => {
clearCachedAgentServerInfo();
});
it("returns config in mock mode without a live backend", async () => {
const config = await OptionService.getConfig();
expect(config.feature_flags.hide_llm_settings).toBe(false);
expect(config.feature_flags.hide_users_page).toBe(true);
expect(config.updated_at).toBeTruthy();
});
it("loads config regardless of agent server version", async () => {
server.use(
http.get("*/server_info", () =>
HttpResponse.json({ uptime: 0, idle_time: 0, version: "1.0.0" }),
),
);
await expect(OptionService.getConfig()).resolves.toMatchObject({
feature_flags: expect.objectContaining({ hide_llm_settings: false }),
});
});
it("loads config even when the server does not advertise a version", async () => {
server.use(
http.get("*/server_info", () =>
HttpResponse.json({ uptime: 0, idle_time: 0 }),
),
);
await expect(OptionService.getConfig()).resolves.toMatchObject({
feature_flags: expect.objectContaining({ hide_llm_settings: false }),
});
});
it("throws an unavailable error when the agent server cannot be reached", async () => {
server.use(http.get("*/server_info", () => HttpResponse.error()));
await expect(OptionService.getConfig()).rejects.toMatchObject({
name: AgentServerUnavailableError.name,
message: expect.stringContaining("Could not connect to the configured agent server"),
details: expect.stringContaining("Request failed"),
});
});
it("caches usable_tools from server_info for later tool gating", async () => {
server.use(
http.get("*/server_info", () =>
HttpResponse.json({
uptime: 0,
idle_time: 0,
version: "1.21.1",
usable_tools: ["terminal", "file_editor", "task_tracker"],
}),
),
);
await OptionService.getConfig();
expect(isAgentServerToolAvailable("browser_tool_set")).toBe(false);
expect(isAgentServerToolAvailable("terminal")).toBe(true);
});
it("allows all tools when the server does not advertise tool metadata", async () => {
server.use(
http.get("*/server_info", () =>
HttpResponse.json({
uptime: 0,
idle_time: 0,
version: "1.21.1",
}),
),
);
await OptionService.getConfig();
expect(isAgentServerToolAvailable("browser_tool_set")).toBe(true);
expect(isAgentServerToolAvailable("terminal")).toBe(true);
});
it("returns models from mocked LLM endpoints", async () => {
const models = await OptionService.getModels();
expect(models.models).toContain("openhands/claude-opus-4-5-20251101");
expect(models.models).toContain("openai/gpt-5.5");
expect(models.verified_models).toContain("claude-opus-4-5-20251101");
expect(models.verified_models).toContain("gpt-5.5");
expect(models.verified_providers).toEqual([
"anthropic",
"openai",
"openhands",
]);
expect(models.default_model).toBeTruthy();
});
describe("posthog_client_key", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("returns the key from VITE_POSTHOG_CLIENT_KEY when set", async () => {
vi.stubEnv("VITE_POSTHOG_CLIENT_KEY", "phc_test_key_123");
const config = await OptionService.getConfig();
expect(config.posthog_client_key).toBe("phc_test_key_123");
});
it("returns null when VITE_POSTHOG_CLIENT_KEY is not set", async () => {
// No stub — env var is absent in the test environment by default,
// so import.meta.env.VITE_POSTHOG_CLIENT_KEY is undefined,
// and the ?? null fallback applies.
const config = await OptionService.getConfig();
expect(config.posthog_client_key).toBeNull();
});
});
});