Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
009d1b5
feat: add remote workspace id codec and remote oRPC client
ThomasK33 Feb 3, 2026
00f17a3
Persist remote mux servers in config
ThomasK33 Feb 3, 2026
fb59d02
Add RemoteServersService and oRPC endpoints
ThomasK33 Feb 3, 2026
ced5767
Validate remote server baseUrl when loading config
ThomasK33 Feb 3, 2026
c14c00b
Add RemoteServersService auth token helpers
ThomasK33 Feb 3, 2026
69a1990
remote: make createRemoteClient generic
ThomasK33 Feb 3, 2026
385e6f1
orpc: proxy core workspace endpoints for remote IDs
ThomasK33 Feb 3, 2026
40a224d
orpc: include remote workspaces in global workspace views
ThomasK33 Feb 3, 2026
579540b
feat: add remoteServers.workspaceCreate API
ThomasK33 Feb 3, 2026
5cdd88b
settings: add remote servers section
ThomasK33 Feb 3, 2026
e0e8dc6
ui: allow creating workspaces on remote server
ThomasK33 Feb 3, 2026
57e1556
test: cover remote workspace proxying
ThomasK33 Feb 3, 2026
ac531d8
Fix eslint issues in remote server/ID helpers
ThomasK33 Feb 3, 2026
a6b4d0d
Fix Storybook crash when remoteServers API missing
ThomasK33 Feb 3, 2026
0897b8d
🤖 fix: block open-in-editor for remote workspaces
ThomasK33 Feb 3, 2026
57c9f43
Hide Create-on controls when no remotes
ThomasK33 Feb 3, 2026
4c87273
Only show Create-on when remotes exist
ThomasK33 Feb 3, 2026
6579f0f
Gate remote mux servers behind experiment
ThomasK33 Feb 3, 2026
ea8275c
storybook: add Remote Servers stories
ThomasK33 Feb 3, 2026
e4f1104
ProjectPage: gate archived remote workspaces by experiment
ThomasK33 Feb 3, 2026
8b0a7f1
test: gate Create-on controls behind experiment
ThomasK33 Feb 3, 2026
dd9147b
ui: indicate remote workspaces
ThomasK33 Feb 3, 2026
7914ed0
Polish remote workspace indicators
ThomasK33 Feb 4, 2026
61672e5
Update remote globe tooltip stories
ThomasK33 Feb 4, 2026
4c8c769
Move remote workspace indicator into hover card
ThomasK33 Feb 4, 2026
7a808eb
Fix sidebar remote workspace hovercard play assertion
ThomasK33 Feb 4, 2026
d8476da
storybook: add remote mux server create workspace stories
ThomasK33 Feb 4, 2026
0611881
storybook: stabilize remote server dropdown play test
ThomasK33 Feb 4, 2026
854a1a4
Remove edit-title hint from workspace hover card
ThomasK33 Feb 4, 2026
ee34b1c
🤖 feat: list remote projects for a remote server
ThomasK33 Feb 4, 2026
7417133
🤖 feat: revamp Remote Servers settings UI
ThomasK33 Feb 4, 2026
62d2289
Fix remote workspace archive/unarchive proxy
ThomasK33 Feb 4, 2026
38b0586
Proxy agent discovery for remote workspace IDs
ThomasK33 Feb 4, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React from "react";
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { GlobalWindow } from "happy-dom";
import { cleanup, render, waitFor } from "@testing-library/react";

import type { APIClient } from "@/browser/contexts/API";
import { TooltipProvider } from "@/browser/components/ui/tooltip";
import type { WorkspaceNameState } from "@/browser/hooks/useWorkspaceName";
import { EXPERIMENT_IDS, getExperimentKey } from "@/common/constants/experiments";
import { RUNTIME_MODE, type ParsedRuntime } from "@/common/types/runtime";
import type { RuntimeChoice } from "@/browser/utils/runtimeUi";
import type { RemoteMuxServerConfig } from "@/common/types/project";

import { CreationControls } from "./CreationControls";
import type { RuntimeAvailabilityState } from "./useCreationWorkspace";

interface RemoteMuxServerListEntry {
config: RemoteMuxServerConfig;
hasAuthToken: boolean;
}

let projects = new Map<string, unknown>();

void mock.module("@/browser/contexts/ProjectContext", () => ({
useProjectContext: () => ({ projects }),
}));

void mock.module("@/browser/contexts/WorkspaceContext", () => ({
useWorkspaceContext: () => ({
beginWorkspaceCreation: () => {
// noop for tests
},
}),
}));

let currentApi: { remoteServers: { list: () => Promise<RemoteMuxServerListEntry[]> } } | null =
null;

void mock.module("@/browser/contexts/API", () => ({
useAPI: () => ({
api: (currentApi as unknown as APIClient | null) ?? null,
status: currentApi ? ("connected" as const) : ("connecting" as const),
error: null,
}),
}));

const DEFAULT_NAME_STATE: WorkspaceNameState = {
name: "test-workspace",
title: null,
isGenerating: false,
autoGenerate: false,
error: null,
setAutoGenerate: () => undefined,
setName: () => undefined,
};

const DEFAULT_RUNTIME_AVAILABILITY: RuntimeAvailabilityState = {
status: "loaded",
data: {
local: { available: true },
worktree: { available: true },
ssh: { available: true },
docker: { available: true },
devcontainer: { available: true },
},
};

const DEFAULT_RUNTIME: ParsedRuntime = { mode: RUNTIME_MODE.WORKTREE };

const REMOTE_MUX_SERVERS_EXPERIMENT_KEY = getExperimentKey(EXPERIMENT_IDS.REMOTE_MUX_SERVERS);

function enableRemoteMuxServersExperiment() {
globalThis.window.localStorage.setItem(REMOTE_MUX_SERVERS_EXPERIMENT_KEY, JSON.stringify(true));
}

function Harness(props: { initialCreateOnRemote: boolean }) {
const [createOnRemote, setCreateOnRemote] = React.useState(props.initialCreateOnRemote);
const [remoteServerId, setRemoteServerId] = React.useState<string | null>(null);

return (
<TooltipProvider>
<div>
<div data-testid="createOnRemote">{createOnRemote ? "remote" : "local"}</div>
<CreationControls
branches={["main"]}
branchesLoaded={true}
trunkBranch="main"
onTrunkBranchChange={() => undefined}
selectedRuntime={DEFAULT_RUNTIME}
coderConfigFallback={{}}
sshHostFallback=""
defaultRuntimeMode={RUNTIME_MODE.WORKTREE as RuntimeChoice}
onSelectedRuntimeChange={() => undefined}
onSetDefaultRuntime={() => undefined}
disabled={false}
projectPath="/projects/demo"
projectName="demo"
nameState={DEFAULT_NAME_STATE}
runtimeAvailabilityState={DEFAULT_RUNTIME_AVAILABILITY}
createOnRemote={createOnRemote}
onCreateOnRemoteChange={setCreateOnRemote}
remoteServerId={remoteServerId}
onRemoteServerIdChange={setRemoteServerId}
/>
</div>
</TooltipProvider>
);
}

describe("CreationControls remote server availability", () => {
beforeEach(() => {
globalThis.window = new GlobalWindow() as unknown as Window & typeof globalThis;
globalThis.document = globalThis.window.document;
globalThis.window.localStorage.clear();
projects = new Map([["/projects/demo", {}]]);
});

afterEach(() => {
cleanup();
currentApi = null;
globalThis.window = undefined as unknown as Window & typeof globalThis;
globalThis.document = undefined as unknown as Document;
});

test("does not render Create-on controls when no remote servers are configured", async () => {
enableRemoteMuxServersExperiment();

const listMock = mock(() => Promise.resolve([]));
currentApi = {
remoteServers: {
list: () => listMock(),
},
};

const view = render(<Harness initialCreateOnRemote={true} />);

await waitFor(() => expect(listMock.mock.calls.length).toBe(1));

await waitFor(() => {
expect(view.queryByLabelText("Create on")).toBeNull();
expect(view.getByTestId("createOnRemote").textContent).toBe("local");
});
});

test("renders Create-on controls when at least one remote server is configured", async () => {
enableRemoteMuxServersExperiment();

const listMock = mock(() =>
Promise.resolve([
{
config: {
id: "remote-1",
label: "Remote 1",
baseUrl: "https://example.com",
enabled: true,
projectMappings: [],
},
hasAuthToken: false,
},
] satisfies RemoteMuxServerListEntry[])
);

currentApi = {
remoteServers: {
list: () => listMock(),
},
};

const view = render(<Harness initialCreateOnRemote={false} />);

await waitFor(() => expect(listMock.mock.calls.length).toBe(1));

await waitFor(() => {
expect(view.getByLabelText("Create on")).toBeTruthy();
expect(view.getByTestId("createOnRemote").textContent).toBe("local");
});
});

test("does not render Create-on controls when experiment is disabled (even if remote servers are configured)", async () => {
const listMock = mock(() =>
Promise.resolve([
{
config: {
id: "remote-1",
label: "Remote 1",
baseUrl: "https://example.com",
enabled: true,
projectMappings: [],
},
hasAuthToken: false,
},
] satisfies RemoteMuxServerListEntry[])
);

currentApi = {
remoteServers: {
list: () => listMock(),
},
};

const view = render(<Harness initialCreateOnRemote={true} />);

await waitFor(() => {
expect(view.queryByLabelText("Create on")).toBeNull();
expect(view.getByTestId("createOnRemote").textContent).toBe("local");
});

expect(listMock.mock.calls.length).toBe(0);
});
});
Loading