import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { getEventContent } from "#/components/v1/chat"; import { ActionEvent, ObservationEvent, SecurityRisk } from "#/types/v1/core"; const terminalActionEvent: ActionEvent = { id: "action-1", timestamp: new Date().toISOString(), source: "agent", thought: [{ type: "text", text: "Checking repository status." }], thinking_blocks: [], action: { kind: "TerminalAction", command: "git status", is_input: false, timeout: null, reset: false, }, tool_name: "terminal", tool_call_id: "tool-1", tool_call: { id: "tool-1", type: "function", function: { name: "terminal", arguments: '{"command":"git status"}', }, }, llm_response_id: "response-1", security_risk: SecurityRisk.LOW, summary: "Check repository status", }; const terminalObservationEvent: ObservationEvent = { id: "obs-1", timestamp: new Date().toISOString(), source: "environment", tool_name: "terminal", tool_call_id: "tool-1", action_id: "action-1", observation: { kind: "TerminalObservation", content: [{ type: "text", text: "On branch main" }], command: "git status", exit_code: 0, is_error: false, timeout: false, metadata: { exit_code: 0, pid: 1, username: "wren", hostname: "sandbox", prefix: "", suffix: "", working_dir: "/workspace/project/OpenHands", py_interpreter_path: null, }, }, }; const taskActionEvent: ActionEvent = { id: "action-task-1", timestamp: new Date().toISOString(), source: "agent", thought: [{ type: "text", text: "I'll ask a sub-agent to count files." }], thinking_blocks: [], action: { kind: "TaskAction", description: "count repository files", prompt: "Count how many files are in the repo using git ls-files | wc -l.", subagent_type: "bash-runner", resume: null, }, tool_name: "task", tool_call_id: "tool-task-1", tool_call: { id: "tool-task-1", type: "function", function: { name: "task", arguments: '{"prompt":"Count files","subagent_type":"bash-runner"}', }, }, llm_response_id: "response-task-1", security_risk: SecurityRisk.LOW, summary: "Ask sub-agent to count repository files", }; const taskObservationEvent: ObservationEvent = { id: "obs-task-1", timestamp: new Date().toISOString(), source: "environment", tool_name: "task", tool_call_id: "tool-task-1", action_id: "action-task-1", observation: { kind: "TaskObservation", content: [{ type: "text", text: "tracked_count=1606" }], is_error: false, task_id: "task_00000002", subagent: "bash-runner", status: "completed", }, }; describe("getEventContent", () => { it("uses the action summary as the full action title", () => { const { title } = getEventContent(terminalActionEvent); render({title}); expect(screen.getByText("Check repository status")).toBeInTheDocument(); expect(screen.queryByText("$ git status")).not.toBeInTheDocument(); }); it("falls back to command-based title when summary is missing", () => { const actionWithoutSummary = { ...terminalActionEvent, summary: undefined }; const { title } = getEventContent(actionWithoutSummary); render({title}); // Without i18n loaded, the translation key renders as the raw key expect(screen.getByText("ACTION_MESSAGE$RUN")).toBeInTheDocument(); expect( screen.queryByText("Check repository status"), ).not.toBeInTheDocument(); }); it("returns empty details for file view action instead of 'Unknown event'", () => { const fileViewAction: ActionEvent = { id: "action-2", timestamp: new Date().toISOString(), source: "agent", thought: [], thinking_blocks: [], action: { kind: "FileEditorAction", command: "view", path: "/workspace/README.md", file_text: null, old_str: null, new_str: null, insert_line: null, view_range: null, }, tool_name: "file_editor", tool_call_id: "tool-2", tool_call: { id: "tool-2", type: "function", function: { name: "file_editor", arguments: '{"command":"view","path":"/workspace/README.md"}', }, }, llm_response_id: "response-2", security_risk: SecurityRisk.LOW, }; const { title, details } = getEventContent(fileViewAction); render({title}); expect(screen.getByText("ACTION_MESSAGE$READ")).toBeInTheDocument(); expect(details).toBe(""); }); it("shows action kind for action-like events missing tool_name/tool_call_id", () => { // Simulate an event that has an action object but fails the strict isActionEvent() guard const malformedEvent = { id: "action-3", timestamp: new Date().toISOString(), source: "agent" as const, action: { kind: "FileEditorAction" }, }; const { title, details } = getEventContent(malformedEvent as any); expect(title).toBe("FILEEDITOR"); expect(details).toBe(""); }); it("reuses the action summary as the full paired observation title", () => { const { title } = getEventContent( terminalObservationEvent, terminalActionEvent, ); render({title}); expect(screen.getByText("Check repository status")).toBeInTheDocument(); expect(screen.queryByText("$ git status")).not.toBeInTheDocument(); }); it("renders the sub-agent task card with subagent, task id, query and result", () => { const { details } = getEventContent(taskObservationEvent, taskActionEvent); render(
{details}
); // Labels render as raw i18n keys because i18n is not loaded in tests expect( screen.getByText("SUBAGENT_OBSERVATION$SUBAGENT:"), ).toBeInTheDocument(); expect(screen.getByText("bash-runner")).toBeInTheDocument(); expect( screen.getByText("SUBAGENT_OBSERVATION$TASK_ID:"), ).toBeInTheDocument(); expect(screen.getByText("task_00000002")).toBeInTheDocument(); expect(screen.getByText("SUBAGENT_OBSERVATION$QUERY")).toBeInTheDocument(); expect( screen.getByText( "Count how many files are in the repo using git ls-files | wc -l.", ), ).toBeInTheDocument(); expect(screen.getByText("SUBAGENT_OBSERVATION$RESULT")).toBeInTheDocument(); expect(screen.getByText("tracked_count=1606")).toBeInTheDocument(); }); it("falls back to the task title key when the action summary is missing", () => { const actionWithoutSummary = { ...taskActionEvent, summary: undefined }; const { title } = getEventContent( taskObservationEvent, actionWithoutSummary, ); render({title}); expect(screen.getByText("OBSERVATION_MESSAGE$TASK")).toBeInTheDocument(); }); it("shows the subagent and query in the in-progress task action card", () => { const { details } = getEventContent(taskActionEvent); expect(details).toContain("**Subagent:** `bash-runner`"); expect(details).toContain( "Count how many files are in the repo using git ls-files | wc -l.", ); }); it("renders image content in the sub-agent result instead of dropping it", () => { const observationWithImage: ObservationEvent = { ...taskObservationEvent, observation: { kind: "TaskObservation", content: [ { type: "text", text: "Here is a screenshot:" }, { type: "image", image_urls: ["data:image/png;base64,abc123"] }, ], is_error: false, task_id: "task_00000002", subagent: "bash-runner", status: "completed", }, }; const { details } = getEventContent(observationWithImage, taskActionEvent); const { container } = render(
{details}
); expect(screen.getByText("Here is a screenshot:")).toBeInTheDocument(); const image = container.querySelector("img"); expect(image).toHaveAttribute("src", "data:image/png;base64,abc123"); }); it("omits the query section when the task observation has no paired action", () => { const { details } = getEventContent(taskObservationEvent); render(
{details}
); expect(screen.getByText("tracked_count=1606")).toBeInTheDocument(); expect( screen.queryByText("SUBAGENT_OBSERVATION$QUERY"), ).not.toBeInTheDocument(); }); });