diff --git a/__tests__/components/backends/manage-backends-modal.test.tsx b/__tests__/components/backends/manage-backends-modal.test.tsx index 420ca4f264..f8e25e8cca 100644 --- a/__tests__/components/backends/manage-backends-modal.test.tsx +++ b/__tests__/components/backends/manage-backends-modal.test.tsx @@ -83,7 +83,6 @@ function TestSeed({ const ctx = useActiveBackendContext(); React.useEffect(() => { onMount(ctx); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return children as React.ReactElement; } @@ -128,6 +127,9 @@ beforeEach(() => { afterEach(() => { window.localStorage.clear(); vi.restoreAllMocks(); + vi.unstubAllEnvs(); + delete (window as unknown as Record) + .__AGENT_CANVAS_LOCK_TO_CLOUD__; __resetActiveStoreForTests(); __resetHealthStoreForTests(); }); @@ -229,6 +231,13 @@ describe("ManageBackendsModal", () => { }); it("opens an edit form pre-filled with the row's backend, and persists changes via updateBackend", async () => { + // These tests exercise edit-form behavior, not lock behavior; isolate + // them from a local .env that sets VITE_LOCK_TO_CLOUD and would hide the + // pencil button this test depends on. + vi.stubEnv("VITE_LOCK_TO_CLOUD", ""); + delete (window as unknown as Record) + .__AGENT_CANVAS_LOCK_TO_CLOUD__; + const user = userEvent.setup(); let backendId = ""; @@ -279,6 +288,10 @@ describe("ManageBackendsModal", () => { }); it("preserves kind:cloud when renaming a cloud backend on a custom domain", async () => { + vi.stubEnv("VITE_LOCK_TO_CLOUD", ""); + delete (window as unknown as Record) + .__AGENT_CANVAS_LOCK_TO_CLOUD__; + const user = userEvent.setup(); let backendId = ""; @@ -328,6 +341,10 @@ describe("ManageBackendsModal", () => { }); it("closes the edit form when the header close button is clicked", async () => { + vi.stubEnv("VITE_LOCK_TO_CLOUD", ""); + delete (window as unknown as Record) + .__AGENT_CANVAS_LOCK_TO_CLOUD__; + const user = userEvent.setup(); renderWithProviders( @@ -612,4 +629,115 @@ describe("BackendRow", () => { cloudBackend.host, ); }); + + it("renders edit and remove buttons when the deployment is not locked to a cloud host", async () => { + // The repo's .env may set VITE_LOCK_TO_CLOUD; clear it explicitly so this + // test exercises the unlocked code path regardless of local env state. + vi.stubEnv("VITE_LOCK_TO_CLOUD", ""); + delete (window as unknown as Record) + .__AGENT_CANVAS_LOCK_TO_CLOUD__; + + const user = userEvent.setup(); + const onEdit = vi.fn(); + const onRemove = vi.fn(); + + renderInQueryClient( +
    + +
, + ); + + const row = screen.getByTestId(`manage-backends-row-${cloudBackend.name}`); + const editButton = within(row).getByTestId( + `manage-backends-edit-${cloudBackend.name}`, + ); + const removeButton = within(row).getByTestId( + `manage-backends-remove-${cloudBackend.name}`, + ); + + expect(editButton).toHaveAccessibleName("BACKEND$EDIT"); + expect(removeButton).toHaveAccessibleName("BACKEND$REMOVE"); + + await user.click(editButton); + await user.click(removeButton); + expect(onEdit).toHaveBeenCalledTimes(1); + expect(onRemove).toHaveBeenCalledTimes(1); + }); + + it("hides edit and remove buttons when locked to a cloud host via VITE_LOCK_TO_CLOUD", () => { + vi.stubEnv("VITE_LOCK_TO_CLOUD", "https://cloud.example.com"); + const onEdit = vi.fn(); + const onRemove = vi.fn(); + + renderInQueryClient( +
    + +
, + ); + + const row = screen.getByTestId(`manage-backends-row-${cloudBackend.name}`); + expect( + within(row).queryByTestId(`manage-backends-edit-${cloudBackend.name}`), + ).not.toBeInTheDocument(); + expect( + within(row).queryByTestId(`manage-backends-remove-${cloudBackend.name}`), + ).not.toBeInTheDocument(); + }); + + it("hides edit and remove buttons when locked to a cloud host via window.__AGENT_CANVAS_LOCK_TO_CLOUD__", () => { + ( + window as unknown as Record + ).__AGENT_CANVAS_LOCK_TO_CLOUD__ = "https://cloud.example.com"; + + renderInQueryClient( +
    + +
, + ); + + const row = screen.getByTestId(`manage-backends-row-${cloudBackend.name}`); + expect( + within(row).queryByTestId(`manage-backends-edit-${cloudBackend.name}`), + ).not.toBeInTheDocument(); + expect( + within(row).queryByTestId(`manage-backends-remove-${cloudBackend.name}`), + ).not.toBeInTheDocument(); + // Row identity (name + host) is still rendered so the user can see the + // locked backend is selected, just not mutate it. + expect(within(row).getByText(cloudBackend.name)).toBeInTheDocument(); + expect(within(row).getByText(cloudBackend.host)).toBeInTheDocument(); + }); }); diff --git a/src/components/features/backends/backend-form-modal.tsx b/src/components/features/backends/backend-form-modal.tsx index 72e75490b5..3a6fa84526 100644 --- a/src/components/features/backends/backend-form-modal.tsx +++ b/src/components/features/backends/backend-form-modal.tsx @@ -912,7 +912,7 @@ function CloudLoginColumn({ }; return ( -
+
diff --git a/src/components/features/backends/backend-row.tsx b/src/components/features/backends/backend-row.tsx index e2be7ac3db..c2157361f5 100644 --- a/src/components/features/backends/backend-row.tsx +++ b/src/components/features/backends/backend-row.tsx @@ -13,6 +13,7 @@ import { BackendStatusDot } from "./backend-status-dot"; import { BackendVersion } from "./backend-version"; import { DeviceFlowAuth } from "./device-flow-auth"; import { getBackendStatusLabel } from "./backend-status-label"; +import { getLockedCloudHost } from "#/api/agent-server-config"; const ROW_ACTION_BUTTON_CLASS = "inline-flex cursor-pointer items-center justify-center rounded-md p-1 text-muted transition-colors hover:bg-interactive-hover hover:text-white"; @@ -59,6 +60,7 @@ export function BackendRow({ : "text-[var(--oh-muted)]"; const dotStatus = isInvalidApiKey ? false : (health?.isConnected ?? null); const canSelect = health?.isConnected === true && !isInvalidApiKey; + const lockedCloudHost = getLockedCloudHost(); return (
  • ) : null} - - + {!lockedCloudHost && ( + + )} + {!lockedCloudHost && ( + + )}
  • );