Download src/api/event-service/event-service.api.test.ts from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 13.9 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/api/event-service/event-service.api.test.ts
- Command line
-
hf download hf://SaylorTwift/openhands/src/api/event-service/event-service.api.test.ts
-
curl -L -o event-service.api.test.ts https://huggingface.co/SaylorTwift/openhands/resolve/main/src/api/event-service/event-service.api.test.ts
13.9 kB
| import { beforeEach, describe, expect, it, vi } from "vitest"; | |
| const { | |
| buildHttpBaseUrlMock, | |
| callCloudProxyMock, | |
| conversationClientConstructorMock, | |
| getActiveBackendMock, | |
| getAgentServerClientOptionsMock, | |
| getAgentServerHttpClientOptionsMock, | |
| getEventCountMock, | |
| remoteEventsListConstructorMock, | |
| remoteSearchMock, | |
| respondToConfirmationMock, | |
| } = vi.hoisted(() => ({ | |
| buildHttpBaseUrlMock: vi.fn(), | |
| callCloudProxyMock: vi.fn(), | |
| conversationClientConstructorMock: vi.fn(), | |
| getActiveBackendMock: vi.fn(), | |
| getAgentServerClientOptionsMock: vi.fn(), | |
| getAgentServerHttpClientOptionsMock: vi.fn(), | |
| getEventCountMock: vi.fn(), | |
| remoteEventsListConstructorMock: vi.fn(), | |
| remoteSearchMock: vi.fn(), | |
| respondToConfirmationMock: vi.fn(), | |
| })); | |
| vi.mock("@openhands/typescript-client/clients", () => ({ | |
| ConversationClient: class { | |
| constructor(options: unknown) { | |
| conversationClientConstructorMock(options); | |
| } | |
| respondToConfirmation = respondToConfirmationMock; | |
| getEventCount = getEventCountMock; | |
| }, | |
| })); | |
| vi.mock("@openhands/typescript-client/events/remote-events-list", () => ({ | |
| RemoteEventsList: class { | |
| constructor(options: unknown, conversationId: string) { | |
| remoteEventsListConstructorMock(options, conversationId); | |
| } | |
| search = remoteSearchMock; | |
| }, | |
| })); | |
| vi.mock("#/utils/websocket-url", () => ({ | |
| buildHttpBaseUrl: buildHttpBaseUrlMock, | |
| })); | |
| vi.mock("../backend-registry/active-store", () => ({ | |
| getActiveBackend: getActiveBackendMock, | |
| })); | |
| vi.mock("../cloud/proxy", () => ({ callCloudProxy: callCloudProxyMock })); | |
| vi.mock("../agent-server-client-options", () => ({ | |
| getAgentServerClientOptions: getAgentServerClientOptionsMock, | |
| getAgentServerHttpClientOptions: getAgentServerHttpClientOptionsMock, | |
| })); | |
| import EventService from "./event-service.api"; | |
| const cloudBackend = { | |
| id: "cloud-backend", | |
| name: "Cloud", | |
| host: "https://app.example.com", | |
| apiKey: "cloud-api-key", | |
| kind: "cloud", | |
| }; | |
| const localBackend = { | |
| id: "local-backend", | |
| name: "Local", | |
| host: "http://localhost:3000", | |
| apiKey: "local-api-key", | |
| kind: "local", | |
| }; | |
| function useBackend(backend: typeof cloudBackend | typeof localBackend): void { | |
| getActiveBackendMock.mockReturnValue({ backend, orgId: null }); | |
| } | |
| describe("EventService", () => { | |
| beforeEach(() => { | |
| vi.resetAllMocks(); | |
| useBackend(cloudBackend); | |
| buildHttpBaseUrlMock.mockReturnValue("https://runtime.example.com/base"); | |
| getAgentServerClientOptionsMock.mockReturnValue({ | |
| host: "http://local-client.example.com", | |
| apiKey: "client-key", | |
| workingDir: "workspace/project", | |
| }); | |
| getAgentServerHttpClientOptionsMock.mockReturnValue({ | |
| baseUrl: "http://local-http.example.com", | |
| apiKey: "http-key", | |
| timeout: 60_000, | |
| }); | |
| }); | |
| describe("respondToConfirmation", () => { | |
| it("routes cloud confirmations through the runtime proxy with session authentication", async () => { | |
| const request = { accept: false, reason: "Needs human review" }; | |
| callCloudProxyMock.mockResolvedValue({ success: true }); | |
| await expect( | |
| EventService.respondToConfirmation( | |
| "conversation-cloud", | |
| "wss://runtime.example.com/base/api/conversations/conversation-cloud", | |
| request, | |
| "session-key", | |
| ), | |
| ).resolves.toEqual({ success: true }); | |
| expect(buildHttpBaseUrlMock).toHaveBeenCalledWith( | |
| "wss://runtime.example.com/base/api/conversations/conversation-cloud", | |
| ); | |
| expect(callCloudProxyMock).toHaveBeenCalledWith({ | |
| backend: cloudBackend, | |
| method: "POST", | |
| hostOverride: "https://runtime.example.com/base", | |
| path: "/api/conversations/conversation-cloud/events/respond_to_confirmation", | |
| body: request, | |
| authMode: "session-api-key", | |
| sessionApiKey: "session-key", | |
| }); | |
| expect(conversationClientConstructorMock).not.toHaveBeenCalled(); | |
| }); | |
| it("uses the typed conversation client for local confirmations", async () => { | |
| useBackend(localBackend); | |
| respondToConfirmationMock.mockResolvedValue({ success: false }); | |
| await expect( | |
| EventService.respondToConfirmation( | |
| "conversation-local", | |
| "http://localhost:3000/api/conversations/conversation-local", | |
| { accept: true }, | |
| ), | |
| ).resolves.toEqual({ success: false }); | |
| expect(getAgentServerClientOptionsMock).toHaveBeenCalledWith({ | |
| conversationUrl: | |
| "http://localhost:3000/api/conversations/conversation-local", | |
| sessionApiKey: undefined, | |
| }); | |
| expect(conversationClientConstructorMock).toHaveBeenCalledWith({ | |
| host: "http://local-client.example.com", | |
| apiKey: "client-key", | |
| workingDir: "workspace/project", | |
| }); | |
| expect(respondToConfirmationMock).toHaveBeenCalledWith( | |
| "conversation-local", | |
| { accept: true }, | |
| ); | |
| expect(callCloudProxyMock).not.toHaveBeenCalled(); | |
| }); | |
| }); | |
| describe("getEventCount", () => { | |
| it("gets the cloud count from the runtime proxy", async () => { | |
| callCloudProxyMock.mockResolvedValue(17); | |
| await expect( | |
| EventService.getEventCount( | |
| "conversation-cloud", | |
| "https://runtime.example.com/api/conversations/conversation-cloud", | |
| null, | |
| ), | |
| ).resolves.toBe(17); | |
| expect(buildHttpBaseUrlMock).toHaveBeenCalledWith( | |
| "https://runtime.example.com/api/conversations/conversation-cloud", | |
| ); | |
| expect(callCloudProxyMock).toHaveBeenCalledWith({ | |
| backend: cloudBackend, | |
| method: "GET", | |
| hostOverride: "https://runtime.example.com/base", | |
| path: "/api/conversations/conversation-cloud/events/count", | |
| authMode: "session-api-key", | |
| sessionApiKey: null, | |
| }); | |
| expect(conversationClientConstructorMock).not.toHaveBeenCalled(); | |
| }); | |
| it("gets the local count from the typed conversation client", async () => { | |
| useBackend(localBackend); | |
| getEventCountMock.mockResolvedValue(8); | |
| await expect( | |
| EventService.getEventCount( | |
| "conversation-local", | |
| "http://localhost:3000/api/conversations/conversation-local", | |
| "session-key", | |
| ), | |
| ).resolves.toBe(8); | |
| expect(getAgentServerClientOptionsMock).toHaveBeenCalledWith({ | |
| conversationUrl: | |
| "http://localhost:3000/api/conversations/conversation-local", | |
| sessionApiKey: "session-key", | |
| }); | |
| expect(conversationClientConstructorMock).toHaveBeenCalledWith({ | |
| host: "http://local-client.example.com", | |
| apiKey: "client-key", | |
| workingDir: "workspace/project", | |
| }); | |
| expect(getEventCountMock).toHaveBeenCalledWith("conversation-local"); | |
| expect(callCloudProxyMock).not.toHaveBeenCalled(); | |
| }); | |
| }); | |
| describe("searchEvents", () => { | |
| it("caps cloud limits and forwards every supported history filter", async () => { | |
| const event = { id: "event-1" }; | |
| callCloudProxyMock.mockResolvedValue({ | |
| items: [event], | |
| next_page_id: "page-2", | |
| }); | |
| await expect( | |
| EventService.searchEvents("conversation-cloud", null, null, { | |
| limit: 250, | |
| pageId: "page-1", | |
| sortOrder: "TIMESTAMP_DESC", | |
| timestampGte: "2026-07-12T00:00:00.000Z", | |
| timestampLt: "2026-07-13T00:00:00.000Z", | |
| }), | |
| ).resolves.toEqual({ items: [event], next_page_id: "page-2" }); | |
| expect(callCloudProxyMock).toHaveBeenCalledWith({ | |
| backend: cloudBackend, | |
| method: "GET", | |
| path: "/api/v1/conversation/conversation-cloud/events/search?limit=100&sort_order=TIMESTAMP_DESC&page_id=page-1×tamp__gte=2026-07-12T00%3A00%3A00.000Z×tamp__lt=2026-07-13T00%3A00%3A00.000Z", | |
| }); | |
| }); | |
| it("uses default cloud options and normalizes a missing response", async () => { | |
| callCloudProxyMock.mockResolvedValue(undefined); | |
| await expect( | |
| EventService.searchEvents("conversation-cloud"), | |
| ).resolves.toEqual({ items: [], next_page_id: null }); | |
| expect(callCloudProxyMock).toHaveBeenCalledWith({ | |
| backend: cloudBackend, | |
| method: "GET", | |
| path: "/api/v1/conversation/conversation-cloud/events/search?limit=100", | |
| }); | |
| }); | |
| it("preserves zero as an explicit cloud limit", async () => { | |
| callCloudProxyMock.mockResolvedValue({}); | |
| await expect( | |
| EventService.searchEvents("conversation-cloud", null, null, { | |
| limit: 0, | |
| }), | |
| ).resolves.toEqual({ items: [], next_page_id: null }); | |
| expect(callCloudProxyMock).toHaveBeenCalledWith({ | |
| backend: cloudBackend, | |
| method: "GET", | |
| path: "/api/v1/conversation/conversation-cloud/events/search?limit=0", | |
| }); | |
| }); | |
| it("rethrows cloud failures when no pagination filter was requested", async () => { | |
| const searchError = new Error("cloud unavailable"); | |
| callCloudProxyMock.mockRejectedValue(searchError); | |
| await expect( | |
| EventService.searchEvents("conversation-cloud", null, null, { | |
| limit: 25, | |
| }), | |
| ).rejects.toBe(searchError); | |
| }); | |
| it("rethrows filtered cloud failures for completeness-sensitive callers", async () => { | |
| const paginationError = new Error("pagination unsupported"); | |
| callCloudProxyMock.mockRejectedValue(paginationError); | |
| await expect( | |
| EventService.searchEvents("conversation-cloud", null, null, { | |
| pageId: "page-2", | |
| strictPagination: true, | |
| }), | |
| ).rejects.toBe(paginationError); | |
| }); | |
| it("retains the empty-page fallback for ordinary cloud pagination", async () => { | |
| callCloudProxyMock.mockRejectedValue(new Error("pagination unsupported")); | |
| const warn = vi | |
| .spyOn(console, "warn") | |
| .mockImplementation(() => undefined); | |
| await expect( | |
| EventService.searchEvents("conversation-cloud", null, null, { | |
| timestampLt: "2026-07-10T12:34:56.000Z", | |
| }), | |
| ).resolves.toEqual({ items: [], next_page_id: null }); | |
| expect(warn).toHaveBeenCalledWith( | |
| "[EventService] Cloud backend doesn't support pagination filters. " + | |
| "Falling back to initial load only. " + | |
| "Server needs OpenHands/OpenHands#14399.", | |
| ); | |
| warn.mockRestore(); | |
| }); | |
| it("treats a sort-only cloud failure as unsupported filtered pagination", async () => { | |
| callCloudProxyMock.mockRejectedValue(new Error("pagination unsupported")); | |
| const warn = vi | |
| .spyOn(console, "warn") | |
| .mockImplementation(() => undefined); | |
| await expect( | |
| EventService.searchEvents("conversation-cloud", null, null, { | |
| sortOrder: "TIMESTAMP_DESC", | |
| }), | |
| ).resolves.toEqual({ items: [], next_page_id: null }); | |
| expect(callCloudProxyMock).toHaveBeenCalledWith({ | |
| backend: cloudBackend, | |
| method: "GET", | |
| path: "/api/v1/conversation/conversation-cloud/events/search?limit=100&sort_order=TIMESTAMP_DESC", | |
| }); | |
| expect(warn).toHaveBeenCalledOnce(); | |
| warn.mockRestore(); | |
| }); | |
| it("recognizes a lower timestamp bound as a cloud pagination filter", async () => { | |
| callCloudProxyMock.mockResolvedValue({ | |
| items: [], | |
| next_page_id: null, | |
| }); | |
| await EventService.searchEvents("conversation-cloud", null, null, { | |
| timestampGte: "2026-07-10T12:34:56.000Z", | |
| }); | |
| expect(callCloudProxyMock).toHaveBeenCalledWith({ | |
| backend: cloudBackend, | |
| method: "GET", | |
| path: "/api/v1/conversation/conversation-cloud/events/search?limit=100×tamp__gte=2026-07-10T12%3A34%3A56.000Z", | |
| }); | |
| }); | |
| it("passes all local filters to the remote events list", async () => { | |
| useBackend(localBackend); | |
| const event = { id: "event-local" }; | |
| remoteSearchMock.mockResolvedValue({ | |
| items: [event], | |
| next_page_id: "local-page-2", | |
| }); | |
| await expect( | |
| EventService.searchEvents( | |
| "conversation-local", | |
| "http://localhost:3000/api/conversations/conversation-local", | |
| "session-key", | |
| { | |
| limit: 40, | |
| pageId: "local-page-1", | |
| sortOrder: "TIMESTAMP", | |
| timestampGte: "2026-07-11T00:00:00.000Z", | |
| timestampLt: "2026-07-12T00:00:00.000Z", | |
| }, | |
| ), | |
| ).resolves.toEqual({ | |
| items: [event], | |
| next_page_id: "local-page-2", | |
| }); | |
| expect(getAgentServerHttpClientOptionsMock).toHaveBeenCalledWith({ | |
| conversationUrl: | |
| "http://localhost:3000/api/conversations/conversation-local", | |
| sessionApiKey: "session-key", | |
| }); | |
| expect(remoteEventsListConstructorMock).toHaveBeenCalledWith( | |
| { | |
| baseUrl: "http://local-http.example.com", | |
| apiKey: "http-key", | |
| timeout: 60_000, | |
| }, | |
| "conversation-local", | |
| ); | |
| expect(remoteSearchMock).toHaveBeenCalledWith({ | |
| limit: 40, | |
| page_id: "local-page-1", | |
| sort_order: "TIMESTAMP", | |
| timestamp__gte: "2026-07-11T00:00:00.000Z", | |
| timestamp__lt: "2026-07-12T00:00:00.000Z", | |
| }); | |
| expect(callCloudProxyMock).not.toHaveBeenCalled(); | |
| }); | |
| it("uses default local options and normalizes a missing page", async () => { | |
| useBackend(localBackend); | |
| remoteSearchMock.mockResolvedValue(undefined); | |
| await expect( | |
| EventService.searchEvents("conversation-local"), | |
| ).resolves.toEqual({ items: [], next_page_id: null }); | |
| expect(getAgentServerHttpClientOptionsMock).toHaveBeenCalledWith({ | |
| conversationUrl: undefined, | |
| sessionApiKey: undefined, | |
| }); | |
| expect(remoteSearchMock).toHaveBeenCalledWith({ limit: 100 }); | |
| }); | |
| }); | |
| }); | |