import { describe, expect, it, vi } from "vitest"; import type { ActionEvent, AgentErrorEvent, MessageEvent, ObservationEvent, } from "#/types/agent-server/core"; import type { ExecuteBashAction, MCPToolAction, TaskAction, } from "#/types/agent-server/core/base/action"; import type { ExecuteBashObservation, TaskObservation, } from "#/types/agent-server/core/base/observation"; import type { StreamingDeltaEvent } from "#/types/agent-server/core/events/streaming-delta-event"; import { SecurityRisk } from "#/types/agent-server/core/base/common"; import { eventsToHtml, eventsToMarkdown } from "."; vi.mock("#/i18n", () => ({ default: { exists: () => false, language: "en", resolvedLanguage: "en", t: (key: string, values?: Record) => { const translations: Record = { ACTION_MESSAGE$ACP_TOOL: "Use tool", CHAT_INTERFACE$ASSISTANT: "Assistant", COMMON$ERROR: "Error", TASK$QUERY: "Query", TASK$RESULT: "Result", TASK$SUBAGENT: "Subagent", TASK$TASK_ID: "Task ID", TRANSCRIPT_EXPORT$DEFAULT_TITLE: "Conversation transcript", TRANSCRIPT_EXPORT$MODEL: "Model", TRANSCRIPT_EXPORT$TOOL: "Tool", TRANSCRIPT_EXPORT$USER: "User", }; return translations[key] ?? values?.name ?? key; }, }, })); const timestamp = "2026-07-10T12:34:56.000Z"; const userMessage: MessageEvent = { id: "user-1", timestamp, source: "user", llm_message: { role: "user", content: [{ type: "text", text: "Please diagnose **the failure**." }], }, activated_skills: [], extended_content: [], }; const assistantMessage: MessageEvent = { id: "assistant-1", timestamp: "2026-07-10T12:35:00.000Z", source: "agent", llm_message: { role: "assistant", content: [{ type: "text", text: "The service is healthy now." }], }, activated_skills: [], extended_content: [], }; const terminalAction: ActionEvent = { id: "action-1", timestamp: "2026-07-10T12:34:57.000Z", source: "agent", thought: [], thinking_blocks: [], action: { kind: "ExecuteBashAction", command: "npm test", 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: "{}" }, }, llm_response_id: "response-1", security_risk: SecurityRisk.LOW, summary: "Run the unit tests", }; const terminalObservation = ( output: string, ): ObservationEvent => ({ id: "observation-1", timestamp: "2026-07-10T12:34:58.000Z", source: "environment", tool_name: "terminal", tool_call_id: "tool-1", action_id: "action-1", observation: { kind: "ExecuteBashObservation", command: "npm test", content: [{ type: "text", text: output }], exit_code: 0, error: false, timeout: false, metadata: { exit_code: 0, pid: 123, username: "openhands", hostname: "sandbox", working_dir: "/workspace/project", py_interpreter_path: null, prefix: "", suffix: "", }, }, }); const defaultOptions = { includeToolDetails: true, includeTimestamps: true, title: "Debug session", model: "anthropic/claude-sonnet", }; describe("conversation transcript export", () => { it("exports messages and collapsed tool details as GitHub-ready Markdown", () => { const markdown = eventsToMarkdown( [ userMessage, terminalAction, terminalObservation("3 tests passed"), assistantMessage, ], defaultOptions, ); expect(markdown).toContain("# Debug session"); expect(markdown).toContain("**Model:** anthropic/claude-sonnet"); expect(markdown).toContain("## User"); expect(markdown).toContain("Please diagnose **the failure**."); expect(markdown).toContain("
"); expect(markdown).toContain("Run the unit tests"); expect(markdown).toContain("3 tests passed"); expect(markdown).toContain("## Assistant"); }); it("honors tool-detail and timestamp options in both formats", () => { const events = [userMessage, terminalAction, terminalObservation("passed")]; const options = { ...defaultOptions, includeToolDetails: false, includeTimestamps: false, }; const markdown = eventsToMarkdown(events, options); const html = eventsToHtml(events, options); expect(markdown).toContain("Tool: Run the unit tests"); expect(markdown).not.toContain("
"); expect(markdown).not.toContain("passed"); expect(markdown).not.toContain(timestamp); expect(html).not.toContain("
"); expect(html).not.toContain("passed"); expect(html).not.toContain(" { const errorEvent: AgentErrorEvent = { id: "error-1", timestamp, kind: "AgentErrorEvent", source: "agent", tool_name: "terminal", tool_call_id: "tool-error", error: "Command failed with exit code 1", }; const markdown = eventsToMarkdown( [terminalAction, terminalObservation("x".repeat(1200)), errorEvent], defaultOptions, ); expect(markdown).toContain("## Error"); expect(markdown).toContain("> Command failed with exit code 1"); expect(markdown).toContain(`${"x".repeat(1000)}...`); expect(markdown).not.toContain("x".repeat(1001)); }); it("exports assistant narration once when an observation replaces its action", () => { const narratedAction: ActionEvent = { ...terminalAction, thought: [{ type: "text", text: "I checked the failing service." }], reasoning_content: "The logs point to a stale process.", }; const markdown = eventsToMarkdown( [narratedAction, terminalObservation("passed")], defaultOptions, ); expect(markdown.match(/I checked the failing service\./g)).toHaveLength(1); expect(markdown.match(/The logs point to a stale process\./g)).toHaveLength( 1, ); }); it("exports streaming reasoning with the same inline-think split as chat", () => { const streamingEvent: StreamingDeltaEvent = { id: "streaming-1", timestamp, source: "agent", kind: "StreamingDeltaEvent", reasoning_content: "Separate reasoning", content: "Inline reasoningVisible answer", }; const markdown = eventsToMarkdown([streamingEvent], defaultOptions); expect(markdown).toContain("Separate reasoning"); expect(markdown).toContain("Inline reasoning"); expect(markdown).toContain("Visible answer"); expect(markdown).not.toContain("<think>"); }); it("splits inline reasoning from finalized assistant messages", () => { const messageEvent: MessageEvent = { ...assistantMessage, llm_message: { role: "assistant", content: [ { type: "text", text: "Reloaded reasoningReloaded answer", }, ], }, }; const markdown = eventsToMarkdown([messageEvent], defaultOptions); expect(markdown).toContain("Reloaded reasoning"); expect(markdown).toContain("Reloaded answer"); expect(markdown).not.toContain("<think>"); }); it("exports task inputs and results through their display projection", () => { const taskAction: ActionEvent = { ...terminalAction, id: "task-action", tool_name: "task", tool_call_id: "task-call", tool_call: { id: "task-call", type: "function", function: { name: "task", arguments: "{}" }, }, action: { kind: "TaskAction", prompt: "Inspect the authentication flow", subagent_type: "security", description: "Inspect authentication", }, summary: "Delegate authentication review", }; const taskObservation: ObservationEvent = { id: "task-observation", timestamp: "2026-07-10T12:34:59.000Z", source: "environment", tool_name: "task", tool_call_id: "task-call", action_id: "task-action", observation: { kind: "TaskObservation", content: [{ type: "text", text: "No credential leak found." }], task_id: "task-123", subagent: "security", status: "completed", is_error: false, }, }; const markdown = eventsToMarkdown( [taskAction, taskObservation], defaultOptions, ); expect(markdown).toContain("Inspect the authentication flow"); expect(markdown).toContain("No credential leak found."); expect(markdown).toContain("task-123"); }); it("only exports the redacted display projection, never hidden raw tool arguments", () => { const redactedAction: ActionEvent = { ...terminalAction, id: "secret-action", tool_call_id: "secret-tool", action: { kind: "MCPToolAction", data: { api_key: "**********" }, }, tool_call: { id: "secret-tool", type: "function", function: { name: "mcp_call", arguments: '{"api_key":"raw-secret-must-not-export"}', }, }, tool_name: "mcp_call", summary: "Call the configured service", }; const markdown = eventsToMarkdown([redactedAction], defaultOptions); const html = eventsToHtml([redactedAction], defaultOptions); expect(markdown).toContain("**********"); expect(html).toContain("**********"); expect(markdown).not.toContain("raw-secret-must-not-export"); expect(html).not.toContain("raw-secret-must-not-export"); }); it("neutralizes active content in Markdown and HTML", () => { const unsafeMessage: MessageEvent = { ...userMessage, llm_message: { role: "user", content: [ { type: "text", text: " [click](javascript:alert(1))\n[ref]: javascript:alert(2)", }, ], }, }; const markdown = eventsToMarkdown([unsafeMessage], defaultOptions); const html = eventsToHtml([unsafeMessage], defaultOptions); expect(markdown).not.toContain("