Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions __tests__/api/agent-server-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const {

vi.mock("#/api/agent-server-config", () => ({
getAgentServerBaseUrl: vi.fn(() => "http://127.0.0.1:8000"),
resolveBrowserReachableAgentServerBaseUrl: vi.fn((baseUrl: string) =>
baseUrl.replace(/\/+$/, ""),
),
getAgentServerSessionApiKey: vi.fn(() => null),
getAgentServerWorkingDir: mockGetAgentServerWorkingDir,
getConfiguredWorkerUrls: vi.fn(() => []),
Expand Down
89 changes: 89 additions & 0 deletions __tests__/api/agent-server-client-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { getAgentServerClientOptions } from "#/api/agent-server-client-options";
import {
__resetActiveStoreForTests,
setActiveSelection,
setRegisteredBackends,
} from "#/api/backend-registry/active-store";
import type { Backend } from "#/api/backend-registry/types";

const ORIGINAL_LOCATION = window.location;

function mockWindowLocation(url: string) {
Object.defineProperty(window, "location", {
configurable: true,
value: new URL(url),
});
}

beforeEach(() => {
window.localStorage.clear();
__resetActiveStoreForTests();
});

afterEach(() => {
window.localStorage.clear();
__resetActiveStoreForTests();
Object.defineProperty(window, "location", {
configurable: true,
value: ORIGINAL_LOCATION,
});
});

describe("agent server client options", () => {
it("routes stored loopback local backends through the browser origin for proxy-capable runtime browsers", () => {
mockWindowLocation(
"https://work-1-abc.prod-runtime.all-hands.dev/conversations",
);
const backend: Backend = {
id: "local-1",
name: "Local",
host: "http://127.0.0.1:18000",
apiKey: "session-key",
kind: "local",
};
setRegisteredBackends([backend]);
setActiveSelection({ backendId: backend.id, orgId: null });

expect(getAgentServerClientOptions()).toMatchObject({
host: "https://work-1-abc.prod-runtime.all-hands.dev",
apiKey: "session-key",
});
});

it("preserves stored loopback local backends for separately hosted frontends", () => {
mockWindowLocation("https://agent-canvas.example.com/conversations");
const backend: Backend = {
id: "local-1",
name: "Local",
host: "http://127.0.0.1:18000",
apiKey: "session-key",
kind: "local",
};
setRegisteredBackends([backend]);
setActiveSelection({ backendId: backend.id, orgId: null });

expect(getAgentServerClientOptions()).toMatchObject({
host: "http://127.0.0.1:18000",
apiKey: "session-key",
});
});

it("preserves non-loopback local backend hosts", () => {
mockWindowLocation("https://work-1.example.dev/conversations");
const backend: Backend = {
id: "local-1",
name: "Local",
host: "https://agent.example.com/",
apiKey: "session-key",
kind: "local",
};
setRegisteredBackends([backend]);
setActiveSelection({ backendId: backend.id, orgId: null });

expect(getAgentServerClientOptions()).toMatchObject({
host: "https://agent.example.com",
apiKey: "session-key",
});
});
});
31 changes: 28 additions & 3 deletions __tests__/api/agent-server-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getAgentServerFormDefaults,
getAgentServerSessionApiKey,
getAgentServerWorkingDir,
resolveBrowserReachableAgentServerBaseUrl,
saveAgentServerConfig,
shouldLoadPublicSkills,
} from "#/api/agent-server-config";
Expand All @@ -30,14 +31,28 @@ afterEach(() => {
});

describe("agent server config", () => {
it("uses the browser origin when a remote browser is pointed at localhost backend config", () => {
mockWindowLocation("https://work-1.example.dev/settings");
it("uses the browser origin when a proxy-capable runtime browser is pointed at localhost backend config", () => {
mockWindowLocation(
"https://work-1-abc.prod-runtime.all-hands.dev/settings",
);
window.localStorage.setItem(
AGENT_SERVER_CONFIG_STORAGE_KEY,
JSON.stringify({ baseUrl: "http://127.0.0.1:8000" }),
);

expect(getAgentServerBaseUrl()).toBe("https://work-1.example.dev");
expect(getAgentServerBaseUrl()).toBe(
"https://work-1-abc.prod-runtime.all-hands.dev",
);
});

it("preserves loopback backend URLs for separately hosted frontends", () => {
mockWindowLocation("https://agent-canvas.example.com/settings");
window.localStorage.setItem(
AGENT_SERVER_CONFIG_STORAGE_KEY,
JSON.stringify({ baseUrl: "http://127.0.0.1:8000" }),
);

expect(getAgentServerBaseUrl()).toBe("http://127.0.0.1:8000");
});

it("preserves a non-local backend URL from stored config", () => {
Expand All @@ -50,6 +65,16 @@ describe("agent server config", () => {
expect(getAgentServerBaseUrl()).toBe("https://agent.example.com");
});

it("resolves stored loopback backend hosts through the browser origin for proxy-capable runtime browsers", () => {
mockWindowLocation(
"https://work-1-abc.prod-runtime.all-hands.dev/conversations",
);

expect(
resolveBrowserReachableAgentServerBaseUrl("http://localhost:18000"),
).toBe("https://work-1-abc.prod-runtime.all-hands.dev");
});

it("prefills the settings form from environment defaults when local settings are empty", () => {
vi.stubEnv("VITE_BACKEND_BASE_URL", "https://env-agent.example.com/");
vi.stubEnv("VITE_SESSION_API_KEY", "env-session-key");
Expand Down
3 changes: 3 additions & 0 deletions __tests__/api/agent-server-conversation-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ vi.mock("@openhands/typescript-client/clients", async () => {
vi.mock("#/api/agent-server-config", () => ({
DEFAULT_WORKING_DIR: "workspace/project",
getAgentServerBaseUrl: vi.fn(() => "http://localhost:54928"),
resolveBrowserReachableAgentServerBaseUrl: vi.fn((baseUrl: string) =>
baseUrl.replace(/\/+$/, ""),
),
getAgentServerSessionApiKey: vi.fn(() => "test-api-key"),
getAgentServerWorkingDir: vi.fn(() => "/workspace/project/agent-canvas"),
buildConversationWorkingDir: vi.fn(
Expand Down
59 changes: 59 additions & 0 deletions __tests__/api/cloud/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import type { Backend } from "#/api/backend-registry/types";

vi.mock("axios");

const ORIGINAL_LOCATION = window.location;

function mockWindowLocation(url: string) {
Object.defineProperty(window, "location", {
configurable: true,
value: new URL(url),
});
}

const localBackend: Backend = {
id: "local-1",
name: "Local",
Expand Down Expand Up @@ -45,6 +54,10 @@ afterEach(() => {
window.localStorage.clear();
__resetActiveStoreForTests();
vi.mocked(axios.post).mockReset();
Object.defineProperty(window, "location", {
configurable: true,
value: ORIGINAL_LOCATION,
});
});

describe("callCloudProxy X-Org-Id injection", () => {
Expand Down Expand Up @@ -98,4 +111,50 @@ describe("callCloudProxy X-Org-Id injection", () => {
(body as { headers: Record<string, string> }).headers,
).not.toHaveProperty("X-Org-Id");
});

it("posts through the browser origin when the stored local backend is loopback-only on a proxy-capable runtime host", async () => {
mockWindowLocation(
"https://work-1-abc.prod-runtime.all-hands.dev/conversations",
);
setRegisteredBackends([
{ ...localBackend, host: "http://127.0.0.1:18000" },
cloudPersonal,
]);
setActiveSelection({
backendId: cloudPersonal.id,
orgId: null,
});

await callCloudProxy({
backend: cloudPersonal,
method: "GET",
path: "/api/v1/app-conversations/search",
});

const [url] = vi.mocked(axios.post).mock.calls[0]!;
expect(url).toBe(
"https://work-1-abc.prod-runtime.all-hands.dev/api/cloud-proxy",
);
});

it("posts to loopback for separately hosted frontends with a local backend", async () => {
mockWindowLocation("https://agent-canvas.example.com/conversations");
setRegisteredBackends([
{ ...localBackend, host: "http://127.0.0.1:18000" },
cloudPersonal,
]);
setActiveSelection({
backendId: cloudPersonal.id,
orgId: null,
});

await callCloudProxy({
backend: cloudPersonal,
method: "GET",
path: "/api/v1/app-conversations/search",
});

const [url] = vi.mocked(axios.post).mock.calls[0]!;
expect(url).toBe("http://127.0.0.1:18000/api/cloud-proxy");
});
});
60 changes: 60 additions & 0 deletions __tests__/api/device-flow-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ vi.mock("../../src/api/backend-registry/auth", () => ({
}));

const TEST_HOST_URL = "https://app.all-hands.dev";
const ORIGINAL_LOCATION = window.location;

function mockWindowLocation(url: string) {
Object.defineProperty(window, "location", {
configurable: true,
value: new URL(url),
});
}

describe("device-flow-client", () => {
beforeEach(() => {
Expand All @@ -28,6 +36,10 @@ describe("device-flow-client", () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
Object.defineProperty(window, "location", {
configurable: true,
value: ORIGINAL_LOCATION,
});
});

describe("isOpenHandsCloudHost", () => {
Expand Down Expand Up @@ -126,6 +138,54 @@ describe("device-flow-client", () => {
expect(body.host).toBe(TEST_HOST_URL);
});

it("uses the browser origin for the local proxy when a proxy-capable runtime browser has a loopback backend", async () => {
mockWindowLocation(
"https://work-1-abc.prod-runtime.all-hands.dev/settings",
);
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
device_code: "dc",
user_code: "uc",
verification_uri: "v",
verification_uri_complete: "vc",
expires_in: 600,
interval: 5,
}),
});

await startDeviceFlow(TEST_HOST_URL);

expect(fetch).toHaveBeenCalledWith(
"https://work-1-abc.prod-runtime.all-hands.dev/api/cloud-proxy",
expect.any(Object),
);
});

it("uses loopback for the local proxy on separately hosted frontends", async () => {
mockWindowLocation("https://agent-canvas.example.com/settings");
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
device_code: "dc",
user_code: "uc",
verification_uri: "v",
verification_uri_complete: "vc",
expires_in: 600,
interval: 5,
}),
});

await startDeviceFlow(TEST_HOST_URL);

expect(fetch).toHaveBeenCalledWith(
"http://localhost:18000/api/cloud-proxy",
expect.any(Object),
);
});

it("throws DeviceFlowError on HTTP error", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
Expand Down
3 changes: 3 additions & 0 deletions __tests__/api/use-create-conversation-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ vi.mock("@openhands/typescript-client/clients", async () => {
vi.mock("#/api/agent-server-config", () => ({
DEFAULT_WORKING_DIR: "workspace/project",
getAgentServerBaseUrl: vi.fn(() => "http://localhost:54928"),
resolveBrowserReachableAgentServerBaseUrl: vi.fn((baseUrl: string) =>
baseUrl.replace(/\/+$/, ""),
),
getAgentServerSessionApiKey: vi.fn(() => null),
getAgentServerWorkingDir: vi.fn(() => "/workspace/project/agent-canvas"),
buildConversationWorkingDir: vi.fn(
Expand Down
3 changes: 2 additions & 1 deletion src/api/agent-server-client-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { buildHttpBaseUrl } from "#/utils/websocket-url";
import {
getAgentServerSessionApiKey,
getAgentServerWorkingDir,
resolveBrowserReachableAgentServerBaseUrl,
} from "./agent-server-config";
import { getEffectiveLocalBackend } from "./backend-registry/active-store";
import { DEFAULT_LOCAL_BACKEND_ID } from "./backend-registry/default-backend";
Expand All @@ -24,7 +25,7 @@ export interface AgentServerClientOptions {
}

function normalizeHost(host: string): string {
return host.replace(/\/+$/, "");
return resolveBrowserReachableAgentServerBaseUrl(host).replace(/\/+$/, "");
}

function resolveHost(
Expand Down
Loading
Loading