Skip to content

Commit

Permalink
Merge branch 'main' into fix/rolling-condenser-truncation
Browse files Browse the repository at this point in the history
  • Loading branch information
csmith49 authored Feb 18, 2025
2 parents 2b4d517 + 8d097ef commit b98e7ed
Show file tree
Hide file tree
Showing 41 changed files with 3,642 additions and 2,792 deletions.
24 changes: 16 additions & 8 deletions docs/modules/usage/cloud/openhands-cloud.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
# Openhands Cloud

This document provides information about the hosted version of OpenHands.
OpenHands Cloud is the cloud hosted version of OpenHands by All Hands AI.

## Accessing OpenHands Cloud

Currently, users are being admitted to access OpenHands Cloud in waves. To sign up,
[join the waitlist](https://www.all-hands.dev/join-waitlist). Once you are approved, you will get an email with
instructions on how to access it.

## Getting Started

After visiting OpenHands Cloud, you will be asked to connect with your GitHub account:
1. After reading and accepting the terms of service, click `Connect to GitHub`.
2. Then click `Authorize OpenHands by All Hands AI`.
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands by All Hands AI`.
- OpenHands will require some permissions from your GitHub account. To read more about these permissions,
you can click the `Learn more` link on the GitHub authorize page.

## Adding Repositories

You can grant OpenHands specific repository access:
1. Under the `Select a GitHub project` dropdown, select `Add more repositories...`.
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
- Openhands requests short-lived tokens (8-hour expiry) with these permissions:
- Actions: Read and write
- Administration: Read-only
Expand All @@ -17,12 +31,6 @@ After visiting OpenHands Cloud, you will be asked to connect with your GitHub ac
- Pull requests: Read and write
- Webhooks: Read and write
- Workflows: Read and write

## Adding Repositories

You can grant OpenHands specific repository access:
1. Under the `Select a GitHub project` dropdown, select `Add more repositories...`.
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
- Repository access for a user is granted based on:
- Granted permission for the repository.
- User's GitHub permissions (owner/collaborator).
Expand Down
2 changes: 2 additions & 0 deletions docs/modules/usage/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Linux
- Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements)

A system with a modern processor and a minimum of **4GB RAM** is recommended to run OpenHands.

## Prerequisites

<details>
Expand Down
166 changes: 166 additions & 0 deletions frontend/__tests__/components/features/payment/payment-form.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
import OpenHands from "#/api/open-hands";
import { PaymentForm } from "#/components/features/payment/payment-form";

describe("PaymentForm", () => {
const getBalanceSpy = vi.spyOn(OpenHands, "getBalance");
const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession");
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");

const renderPaymentForm = () =>
render(<PaymentForm />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});

beforeEach(() => {
// useBalance hook will return the balance only if the APP_MODE is "saas"
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});
});

afterEach(() => {
vi.clearAllMocks();
});

it("should render the users current balance", async () => {
getBalanceSpy.mockResolvedValue("100.50");
renderPaymentForm();

await waitFor(() => {
const balance = screen.getByTestId("user-balance");
expect(balance).toHaveTextContent("$100.50");
});
});

it("should render the users current balance to two decimal places", async () => {
getBalanceSpy.mockResolvedValue("100");
renderPaymentForm();

await waitFor(() => {
const balance = screen.getByTestId("user-balance");
expect(balance).toHaveTextContent("$100.00");
});
});

test("the user can top-up a specific amount", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.12");

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.12);
});

it("should round the top-up amount to two decimal places", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.125456");

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.13);
});

it("should render the payment method link", async () => {
renderPaymentForm();

screen.getByTestId("payment-methods-link");
});

it("should disable the top-up button if the user enters an invalid amount", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpButton = screen.getByText("Add credit");
expect(topUpButton).toBeDisabled();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, " ");

expect(topUpButton).toBeDisabled();
});

it("should disable the top-up button after submission", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.12");

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(topUpButton).toBeDisabled();
});

describe("prevent submission if", () => {
test("user enters a negative amount", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "-50.12");

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});

test("user enters an empty string", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, " ");

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});

test("user enters a non-numeric value", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "abc");

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});

test("user enters less than the minimum amount", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "20"); // test assumes the minimum is 25

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});
});
});
23 changes: 22 additions & 1 deletion frontend/__tests__/components/settings/settings-input.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { SettingsInput } from "#/components/features/settings/settings-input";

describe("SettingsInput", () => {
Expand Down Expand Up @@ -85,4 +86,24 @@ describe("SettingsInput", () => {

expect(screen.getByText("Start Content")).toBeInTheDocument();
});

it("should call onChange with the input value", async () => {
const onChangeMock = vi.fn();
const user = userEvent.setup();

render(
<SettingsInput
testId="test-input"
label="Test Input"
type="text"
onChange={onChangeMock}
/>,
);

const input = screen.getByTestId("test-input");
await user.type(input, "Test");

expect(onChangeMock).toHaveBeenCalledTimes(4);
expect(onChangeMock).toHaveBeenNthCalledWith(4, "Test");
});
});
17 changes: 10 additions & 7 deletions frontend/__tests__/routes/home.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ describe("Home Screen", () => {
Component: Home,
path: "/",
},
{
Component: SettingsScreen,
path: "/settings",
},
],
},
{
Component: SettingsScreen,
path: "/settings",
},
]);

afterEach(() => {
Expand Down Expand Up @@ -96,6 +96,9 @@ describe("Home Screen", () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub initialEntries={["/"]} />);

const settingsScreen = screen.queryByTestId("settings-screen");
expect(settingsScreen).not.toBeInTheDocument();

const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();

Expand All @@ -104,11 +107,11 @@ describe("Home Screen", () => {
);
await user.click(advancedSettingsButton);

const settingsScreenAfter = await screen.findByTestId("settings-screen");
expect(settingsScreenAfter).toBeInTheDocument();

const settingsModalAfter = screen.queryByTestId("ai-config-modal");
expect(settingsModalAfter).not.toBeInTheDocument();

const settingsScreen = await screen.findByTestId("settings-screen");
expect(settingsScreen).toBeInTheDocument();
});
});
});
83 changes: 83 additions & 0 deletions frontend/__tests__/routes/settings-with-payment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createRoutesStub } from "react-router";
import { renderWithProviders } from "test-utils";
import OpenHands from "#/api/open-hands";
import SettingsScreen from "#/routes/settings";
import { PaymentForm } from "#/components/features/payment/payment-form";
import * as FeatureFlags from "#/utils/feature-flags";

describe("Settings Billing", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true);

const RoutesStub = createRoutesStub([
{
Component: SettingsScreen,
path: "/settings",
children: [
{
Component: () => <PaymentForm />,
path: "/settings/billing",
},
],
},
]);

const renderSettingsScreen = () =>
renderWithProviders(<RoutesStub initialEntries={["/settings"]} />);

afterEach(() => {
vi.clearAllMocks();
});

it("should not render the navbar if OSS mode", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});

renderSettingsScreen();

await waitFor(() => {
const navbar = screen.queryByTestId("settings-navbar");
expect(navbar).not.toBeInTheDocument();
});
});

it("should render the navbar if SaaS mode", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});

renderSettingsScreen();

await waitFor(() => {
const navbar = screen.getByTestId("settings-navbar");
within(navbar).getByText("Account");
within(navbar).getByText("Credits");
});
});

it("should render the billing settings if clicking the credits item", async () => {
const user = userEvent.setup();
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});

renderSettingsScreen();

const navbar = await screen.findByTestId("settings-navbar");
const credits = within(navbar).getByText("Credits");
await user.click(credits);

const billingSection = await screen.findByTestId("billing-settings");
within(billingSection).getByText("Manage Credits");
});
});
Loading

0 comments on commit b98e7ed

Please sign in to comment.