Download __tests__/api/profiles-service.test.ts from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 9.27 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/api/profiles-service.test.ts
- Command line
-
hf download hf://SaylorTwift/openhands/__tests__/api/profiles-service.test.ts
-
curl -L -o profiles-service.test.ts https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/api/profiles-service.test.ts
9.27 kB
| /** | |
| * ProfilesService tests using SDK client mocks. | |
| * | |
| * Note: We mock ProfilesClient directly because the SDK's HttpClient uses | |
| * native fetch that MSW doesn't intercept reliably in Node.js test environments. | |
| * This approach tests that ProfilesService correctly delegates to ProfilesClient | |
| * methods and propagates errors from the SDK. | |
| * | |
| * For full integration testing, use browser-level tests with MSW. | |
| */ | |
| import { ProfilesClient } from "@openhands/typescript-client/clients"; | |
| import { afterEach, describe, expect, it, vi, beforeEach } from "vitest"; | |
| import ProfilesService from "#/api/profiles-service/profiles-service.api"; | |
| // Mock the ProfilesClient from the SDK using vi.hoisted | |
| const { | |
| mockListProfiles, | |
| mockGetProfile, | |
| mockSaveProfile, | |
| mockDeleteProfile, | |
| mockRenameProfile, | |
| mockActivateProfile, | |
| mockValidateProfile, | |
| mockClose, | |
| } = vi.hoisted(() => ({ | |
| mockListProfiles: vi.fn(), | |
| mockGetProfile: vi.fn(), | |
| mockSaveProfile: vi.fn(), | |
| mockDeleteProfile: vi.fn(), | |
| mockRenameProfile: vi.fn(), | |
| mockActivateProfile: vi.fn(), | |
| mockValidateProfile: vi.fn(), | |
| mockClose: vi.fn(), | |
| })); | |
| vi.mock("@openhands/typescript-client/clients", () => ({ | |
| ProfilesClient: vi.fn(function ProfilesClientMock() { | |
| return { | |
| listProfiles: mockListProfiles, | |
| getProfile: mockGetProfile, | |
| saveProfile: mockSaveProfile, | |
| deleteProfile: mockDeleteProfile, | |
| renameProfile: mockRenameProfile, | |
| activateProfile: mockActivateProfile, | |
| validateProfile: mockValidateProfile, | |
| close: mockClose, | |
| }; | |
| }), | |
| })); | |
| vi.mock("#/api/agent-server-client-options", () => ({ | |
| getAgentServerClientOptions: vi.fn(() => ({ | |
| host: "http://localhost:3000", | |
| apiKey: "test-key", | |
| })), | |
| })); | |
| describe("ProfilesService", () => { | |
| beforeEach(() => { | |
| mockListProfiles.mockReset(); | |
| mockGetProfile.mockReset(); | |
| mockSaveProfile.mockReset(); | |
| mockDeleteProfile.mockReset(); | |
| mockRenameProfile.mockReset(); | |
| mockActivateProfile.mockReset(); | |
| mockValidateProfile.mockReset(); | |
| mockClose.mockReset(); | |
| vi.mocked(ProfilesClient).mockClear(); | |
| }); | |
| afterEach(() => { | |
| vi.clearAllMocks(); | |
| }); | |
| describe("listProfiles", () => { | |
| it("calls listProfiles and returns profiles list", async () => { | |
| const mockResponse = { | |
| profiles: [ | |
| { | |
| name: "gpt-4-profile", | |
| model: "openai/gpt-4", | |
| base_url: null, | |
| api_key_set: true, | |
| }, | |
| { | |
| name: "claude-profile", | |
| model: "anthropic/claude-3", | |
| base_url: "https://api.anthropic.com", | |
| api_key_set: false, | |
| }, | |
| ], | |
| active_profile: null, | |
| }; | |
| mockListProfiles.mockResolvedValue(mockResponse); | |
| const result = await ProfilesService.listProfiles(); | |
| expect(mockListProfiles).toHaveBeenCalled(); | |
| expect(result).toEqual(mockResponse); | |
| expect(result.profiles).toHaveLength(2); | |
| }); | |
| it("returns empty profiles array when no profiles exist", async () => { | |
| const mockResponse = { profiles: [], active_profile: null }; | |
| mockListProfiles.mockResolvedValue(mockResponse); | |
| const result = await ProfilesService.listProfiles(); | |
| expect(result.profiles).toEqual([]); | |
| }); | |
| it("propagates errors from the SDK", async () => { | |
| mockListProfiles.mockRejectedValue(new Error("Network error")); | |
| await expect(ProfilesService.listProfiles()).rejects.toThrow( | |
| "Network error", | |
| ); | |
| }); | |
| }); | |
| describe("getProfile", () => { | |
| it("calls getProfile with profile name", async () => { | |
| const mockResponse = { | |
| name: "my-profile", | |
| config: { model: "openai/gpt-4" }, | |
| api_key_set: true, | |
| }; | |
| mockGetProfile.mockResolvedValue(mockResponse); | |
| const result = await ProfilesService.getProfile("my-profile"); | |
| expect(mockGetProfile).toHaveBeenCalledWith("my-profile", {}); | |
| expect(result).toEqual(mockResponse); | |
| }); | |
| it("passes exposeSecrets option when set", async () => { | |
| const mockResponse = { | |
| name: "my-profile", | |
| config: { model: "openai/gpt-4", api_key: "encrypted_..." }, | |
| api_key_set: true, | |
| }; | |
| mockGetProfile.mockResolvedValue(mockResponse); | |
| await ProfilesService.getProfile("my-profile", "encrypted"); | |
| expect(mockGetProfile).toHaveBeenCalledWith("my-profile", { | |
| exposeSecrets: "encrypted", | |
| }); | |
| }); | |
| }); | |
| describe("saveProfile", () => { | |
| it("calls saveProfile with name and request body", async () => { | |
| const mockResponse = { name: "new-profile", message: "Profile saved" }; | |
| mockSaveProfile.mockResolvedValue(mockResponse); | |
| const request = { | |
| llm: { | |
| model: "openai/gpt-4", | |
| api_key: "sk-xxx", | |
| }, | |
| include_secrets: true, | |
| }; | |
| const result = await ProfilesService.saveProfile("new-profile", request); | |
| expect(mockSaveProfile).toHaveBeenCalledWith("new-profile", request); | |
| expect(result).toEqual(mockResponse); | |
| }); | |
| it("saves profile with base_url", async () => { | |
| const mockResponse = { name: "custom-profile", message: "Profile saved" }; | |
| mockSaveProfile.mockResolvedValue(mockResponse); | |
| const request = { | |
| llm: { | |
| model: "openai/gpt-4", | |
| base_url: "https://custom.api.com", | |
| }, | |
| }; | |
| await ProfilesService.saveProfile("custom-profile", request); | |
| expect(mockSaveProfile).toHaveBeenCalledWith("custom-profile", request); | |
| }); | |
| }); | |
| describe("deleteProfile", () => { | |
| it("calls deleteProfile with profile name", async () => { | |
| const mockResponse = { name: "old-profile", message: "Profile deleted" }; | |
| mockDeleteProfile.mockResolvedValue(mockResponse); | |
| const result = await ProfilesService.deleteProfile("old-profile"); | |
| expect(mockDeleteProfile).toHaveBeenCalledWith("old-profile"); | |
| expect(result).toEqual(mockResponse); | |
| }); | |
| }); | |
| describe("renameProfile", () => { | |
| it("calls renameProfile with old and new names", async () => { | |
| const mockResponse = { | |
| name: "renamed-profile", | |
| message: "Profile renamed", | |
| }; | |
| mockRenameProfile.mockResolvedValue(mockResponse); | |
| const result = await ProfilesService.renameProfile( | |
| "old-name", | |
| "renamed-profile", | |
| ); | |
| expect(mockRenameProfile).toHaveBeenCalledWith( | |
| "old-name", | |
| "renamed-profile", | |
| ); | |
| expect(result).toEqual(mockResponse); | |
| }); | |
| }); | |
| describe("activateProfile", () => { | |
| it("calls activateProfile with profile name", async () => { | |
| const mockResponse = { | |
| name: "active-profile", | |
| message: "Profile activated", | |
| llm_applied: true, | |
| }; | |
| mockActivateProfile.mockResolvedValue(mockResponse); | |
| const result = await ProfilesService.activateProfile("active-profile"); | |
| expect(mockActivateProfile).toHaveBeenCalledWith("active-profile"); | |
| expect(result).toEqual(mockResponse); | |
| }); | |
| }); | |
| describe("validateProfile", () => { | |
| const request = { llm: { model: "openai/gpt-4o" }, include_secrets: true }; | |
| it("returns a valid verdict via the typed client", async () => { | |
| mockValidateProfile.mockResolvedValue({ valid: true }); | |
| const result = await ProfilesService.validateProfile("gpt", request); | |
| expect(mockValidateProfile).toHaveBeenCalledWith("gpt", request); | |
| expect(result).toEqual({ valid: true }); | |
| expect(mockClose).toHaveBeenCalled(); | |
| }); | |
| it("returns null when the agent-server reports 404", async () => { | |
| mockValidateProfile.mockRejectedValue( | |
| Object.assign(new Error("HTTP 404"), { status: 404 }), | |
| ); | |
| await expect( | |
| ProfilesService.validateProfile("gpt", request), | |
| ).resolves.toBeNull(); | |
| }); | |
| it("propagates other errors", async () => { | |
| mockValidateProfile.mockRejectedValue( | |
| Object.assign(new Error("HTTP 400"), { status: 400 }), | |
| ); | |
| await expect( | |
| ProfilesService.validateProfile("gpt", request), | |
| ).rejects.toThrow("HTTP 400"); | |
| }); | |
| it.each([429, 503])( | |
| "returns null for transient HTTP %s errors", | |
| async (status) => { | |
| mockValidateProfile.mockRejectedValue( | |
| Object.assign(new Error(`HTTP ${status}`), { status }), | |
| ); | |
| await expect( | |
| ProfilesService.validateProfile("gpt", request), | |
| ).resolves.toBeNull(); | |
| expect(mockClose).toHaveBeenCalled(); | |
| }, | |
| ); | |
| it("returns null for timeout-like errors", async () => { | |
| mockValidateProfile.mockRejectedValue( | |
| new Error("Request failed", { | |
| cause: new DOMException("timed out", "TimeoutError"), | |
| }), | |
| ); | |
| await expect( | |
| ProfilesService.validateProfile("gpt", request), | |
| ).resolves.toBeNull(); | |
| }); | |
| it("returns an invalid verdict with its error", async () => { | |
| const response = { | |
| valid: false, | |
| error: { type: "auth", message: "bad key" }, | |
| }; | |
| mockValidateProfile.mockResolvedValue(response); | |
| await expect( | |
| ProfilesService.validateProfile("gpt", request), | |
| ).resolves.toEqual(response); | |
| }); | |
| }); | |
| }); | |