File size: 4,779 Bytes
fd2c364
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import { fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import SettingsService from "#/api/settings-service/settings-service.api";
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { getAgentSettingValue } from "#/utils/sdk-settings-schema";

const trackSettingsSavedMock = vi.fn();
vi.mock("#/hooks/use-tracking", () => ({
  useTracking: () => ({
    trackSettingsSaved: trackSettingsSavedMock,
  }),
}));

describe("SettingsForm", () => {
  const onCloseMock = vi.fn();
  const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");

  // The persisted llm.model is "openhands/<m>"; ModelSelector splits it into
  // provider ("openhands" → "OpenHands") and model name ("<m>") and feeds
  // them into two autocompletes. extractSettings reconstructs llm.model
  // from those two FormData entries on submit, so we wait for the model
  // autocomplete to be populated before triggering submission — otherwise
  // the spy would be called with model = undefined.
  const expectedModel = getAgentSettingValue(
    DEFAULT_SETTINGS,
    "llm.model",
  ) as string;
  const expectedModelName = expectedModel.split("/").slice(1).join("/");

  beforeEach(() => {
    vi.clearAllMocks();
    saveSettingsSpy.mockResolvedValue(true);
  });

  it("should save the user settings and close the modal when submitted outside a conversation route", async () => {
    renderWithProviders(
      <SettingsForm settings={DEFAULT_SETTINGS} onClose={onCloseMock} />,
      {
        navigation: { currentPath: "/settings" },
      },
    );

    await waitFor(() => {
      expect(screen.getByTestId("llm-model-input")).toHaveValue(
        expectedModelName,
      );
    });

    // The Autocomplete popover may be open at this point; click on the save
    // button can be consumed by the outside-click handler. Submit the form
    // element directly — the production onSubmit handler is what we're
    // exercising here, not the click semantics.
    fireEvent.submit(screen.getByTestId("settings-form"));

    await waitFor(() => {
      expect(saveSettingsSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          agent_settings_diff: expect.objectContaining({
            llm: expect.objectContaining({
              model: expectedModel,
            }),
          }),
        }),
      );
    });
    await waitFor(() => {
      expect(onCloseMock).toHaveBeenCalled();
    });
  });

  it("should confirm before saving when submitted from a conversation route", async () => {
    const user = userEvent.setup();
    renderWithProviders(
      <SettingsForm settings={DEFAULT_SETTINGS} onClose={onCloseMock} />,
      {
        navigation: {
          currentPath: "/conversations/test-conversation-id",
          conversationId: "test-conversation-id",
        },
      },
    );

    await waitFor(() => {
      expect(screen.getByTestId("llm-model-input")).toHaveValue(
        expectedModelName,
      );
    });

    fireEvent.submit(screen.getByTestId("settings-form"));

    expect(saveSettingsSpy).not.toHaveBeenCalled();

    // The confirm modal mounts after the form submit handler runs the
    // conversation-route branch, so wait for it instead of using a sync
    // getByRole.
    const confirmButton = await screen.findByRole("button", {
      name: /BUTTON\$END_SESSION|end session/i,
    });

    await user.click(confirmButton);

    await waitFor(() => {
      expect(saveSettingsSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          agent_settings_diff: expect.objectContaining({
            llm: expect.objectContaining({
              model: expectedModel,
            }),
          }),
        }),
      );
    });
    await waitFor(() => {
      expect(onCloseMock).toHaveBeenCalled();
    });
  });

  it("calls trackSettingsSaved with LLM details when form is submitted", async () => {
    renderWithProviders(
      <SettingsForm settings={DEFAULT_SETTINGS} onClose={onCloseMock} />,
      {
        navigation: { currentPath: "/settings" },
      },
    );

    await waitFor(() => {
      expect(screen.getByTestId("llm-model-input")).toHaveValue(
        expectedModelName,
      );
    });

    fireEvent.submit(screen.getByTestId("settings-form"));

    await waitFor(() => {
      expect(trackSettingsSavedMock).toHaveBeenCalledWith(
        expect.objectContaining({
          llmApiKeySet: expect.stringMatching(/^(SET|UNSET)$/),
          searchApiKeySet: expect.stringMatching(/^(SET|UNSET)$/),
        }),
      );
    });
  });
});