| import React, { PropsWithChildren } from "react"; |
| import { RenderOptions, render } from "@testing-library/react"; |
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; |
| import { I18nextProvider, initReactI18next } from "react-i18next"; |
| import i18n from "i18next"; |
| import { vi } from "vitest"; |
| import { AxiosError } from "axios"; |
| import { |
| ActionEvent, |
| MessageEvent, |
| ObservationEvent, |
| PlanningFileEditorObservation, |
| } from "#/types/agent-server/core"; |
| import { |
| NavigationProvider, |
| type NavigationContextValue, |
| } from "#/context/navigation-context"; |
| import { SecurityRisk } from "#/types/agent-server/core"; |
|
|
| export const useParamsMock = vi.fn(() => ({ |
| conversationId: "test-conversation-id", |
| })); |
|
|
| |
| vi.mock("react-router", async () => { |
| const actual = |
| await vi.importActual<typeof import("react-router")>("react-router"); |
| return { |
| ...actual, |
| useParams: useParamsMock, |
| useRevalidator: () => ({ |
| revalidate: vi.fn(), |
| }), |
| }; |
| }); |
|
|
| |
| i18n.use(initReactI18next).init({ |
| lng: "en", |
| fallbackLng: "en", |
| ns: ["translation", "openhands"], |
| defaultNS: "translation", |
| resources: { |
| en: { |
| translation: {}, |
| openhands: {}, |
| }, |
| }, |
| interpolation: { |
| escapeValue: false, |
| }, |
| returnEmptyString: false, |
| missingKeyHandler: false, |
| saveMissing: false, |
| }); |
|
|
| const createNavigationValue = ( |
| overrides: Partial<NavigationContextValue> = {}, |
| ): NavigationContextValue => ({ |
| currentPath: "/", |
| conversationId: "test-conversation-id", |
| isNavigating: false, |
| navigate: vi.fn(), |
| ...overrides, |
| }); |
|
|
| |
| interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> { |
| navigation?: Partial<NavigationContextValue>; |
| } |
|
|
| |
| |
| export function renderWithProviders( |
| ui: React.ReactElement, |
| renderOptions: ExtendedRenderOptions = {}, |
| ) { |
| const { navigation, ...rtlRenderOptions } = renderOptions; |
| const navigationValue = createNavigationValue(navigation); |
|
|
| function Wrapper({ children }: PropsWithChildren) { |
| return ( |
| <QueryClientProvider |
| client={ |
| new QueryClient({ |
| defaultOptions: { queries: { retry: false } }, |
| }) |
| } |
| > |
| <I18nextProvider i18n={i18n}> |
| <NavigationProvider value={navigationValue}> |
| {children} |
| </NavigationProvider> |
| </I18nextProvider> |
| </QueryClientProvider> |
| ); |
| } |
| return render(ui, { wrapper: Wrapper, ...rtlRenderOptions }); |
| } |
|
|
| export const createAxiosNotFoundErrorObject = () => |
| new AxiosError( |
| "Request failed with status code 404", |
| "ERR_BAD_REQUEST", |
| undefined, |
| undefined, |
| { |
| status: 404, |
| statusText: "Not Found", |
| data: { message: "Settings not found" }, |
| headers: {}, |
| |
| config: {}, |
| }, |
| ); |
|
|
| export const createAxiosError = ( |
| status: number, |
| statusText: string, |
| data: unknown, |
| ) => |
| new AxiosError( |
| `Request failed with status code ${status}`, |
| "ERR_BAD_REQUEST", |
| undefined, |
| undefined, |
| { |
| status, |
| statusText, |
| data, |
| headers: {}, |
| |
| config: {}, |
| }, |
| ); |
|
|
| |
| export const createPlanningFileEditorActionEvent = ( |
| id: string, |
| ): ActionEvent => ({ |
| id, |
| timestamp: new Date().toISOString(), |
| source: "agent", |
| thought: [{ type: "text", text: "Planning action" }], |
| thinking_blocks: [], |
| action: { |
| kind: "PlanningFileEditorAction", |
| command: "create", |
| path: "/workspace/PLAN.md", |
| file_text: "Plan content", |
| old_str: null, |
| new_str: null, |
| insert_line: null, |
| view_range: null, |
| }, |
| tool_name: "planning_file_editor", |
| tool_call_id: "call-1", |
| tool_call: { |
| id: "call-1", |
| type: "function", |
| function: { |
| name: "planning_file_editor", |
| arguments: '{"command": "create"}', |
| }, |
| }, |
| llm_response_id: "response-1", |
| security_risk: SecurityRisk.UNKNOWN, |
| }); |
|
|
| |
| export const createOtherActionEvent = (id: string): ActionEvent => ({ |
| id, |
| timestamp: new Date().toISOString(), |
| source: "agent", |
| thought: [{ type: "text", text: "Other action" }], |
| thinking_blocks: [], |
| action: { |
| kind: "ExecuteBashAction", |
| command: "echo test", |
| is_input: false, |
| timeout: null, |
| reset: false, |
| }, |
| tool_name: "execute_bash", |
| tool_call_id: "call-1", |
| tool_call: { |
| id: "call-1", |
| type: "function", |
| function: { |
| name: "execute_bash", |
| arguments: '{"command": "echo test"}', |
| }, |
| }, |
| llm_response_id: "response-1", |
| security_risk: SecurityRisk.UNKNOWN, |
| }); |
|
|
| |
| export const createPlanningObservationEvent = ( |
| id: string, |
| actionId: string = "action-1", |
| ): ObservationEvent<PlanningFileEditorObservation> => ({ |
| id, |
| timestamp: new Date().toISOString(), |
| source: "environment", |
| tool_name: "planning_file_editor", |
| tool_call_id: "call-1", |
| action_id: actionId, |
| observation: { |
| kind: "PlanningFileEditorObservation", |
| content: [{ type: "text", text: "Plan content" }], |
| is_error: false, |
| command: "create", |
| path: "/workspace/PLAN.md", |
| prev_exist: false, |
| old_content: null, |
| new_content: "Plan content", |
| }, |
| }); |
|
|
| |
| export const createUserMessageEvent = (id: string): MessageEvent => ({ |
| id, |
| timestamp: new Date().toISOString(), |
| source: "user", |
| llm_message: { |
| role: "user", |
| content: [{ type: "text", text: "User message" }], |
| }, |
| activated_skills: [], |
| extended_content: [], |
| }); |
|
|