import Foundation
import Testing
@testable import OpenClawChatUI
@Suite("ChatTranscriptExporter")
struct ChatTranscriptExporterTests {
@Test func `exports system transcript rows without leaking internal prompts`() throws {
let wire = Data(#"""
[
{
"role": "user",
"content": [{"type": "text", "text": "[System] Resume the interrupted turn with private context."}],
"timestamp": 0,
"provenance": {"kind": "internal_system", "sourceTool": "main_session_restart_recovery"}
},
{
"role": "user",
"content": [{"type": "text", "text": "[System] Gateway restarted after an update."}],
"timestamp": 500,
"provenance": {"kind": "internal_system", "sourceTool": "restart-sentinel"}
},
{
"role": "system",
"content": [],
"timestamp": 1000,
"__openclaw": {"kind": "compaction", "id": "compact-1", "tokensBefore": 25000, "tokensAfter": 12500}
},
{
"role": "system",
"content": [],
"timestamp": 2000,
"__openclaw": {"kind": "reset", "id": "reset-1"}
},
{
"role": "system",
"content": [{"type": "text", "text": "Unknown marker body"}],
"timestamp": 3000,
"__openclaw": {"kind": "future-marker", "id": "future-1"}
}
]
"""#.utf8)
let messages = try JSONDecoder().decode([OpenClawChatMessage].self, from: wire)
let markdown = ChatTranscriptExporter.markdown(
sessionTitle: "System events",
sessionKey: "agent:main",
messages: messages)
#expect(!markdown.contains("private context"))
#expect(markdown.contains("System · restart recovery"))
#expect(markdown.contains("[System · gateway restarted] Gateway restarted after an update."))
#expect(markdown.contains("[Compacted history · saved 12.5k tokens]"))
#expect(markdown.contains("[Session reset — The earlier conversation was cleared.]"))
#expect(!markdown.contains("Unknown marker body"))
}
@Test func `formats visible messages and attachments`() {
let messages = [
self.message(role: "system", text: "Hidden setup", timestamp: 0),
self.message(role: "user", text: "Hello, 世界 👋", timestamp: 0),
self.message(
role: "assistant",
text: """
Do not export this reasoning.
| Item | Value |
| --- | --- |
| code | `ok` |
```swift
print("hi")
```
""",
timestamp: 1000),
OpenClawChatMessage(
role: "user",
content: [
OpenClawChatMessageContent(
type: "attachment",
text: nil,
mimeType: "text/plain",
fileName: "résumé.txt",
content: nil),
],
timestamp: 2000),
self.message(role: "assistant", text: " ", timestamp: 3000),
]
let markdown = ChatTranscriptExporter.markdown(
sessionTitle: "Project 🐾",
sessionKey: "agent:main",
messages: messages)
#expect(markdown == """
# Project 🐾
### User — 1970-01-01T00:00:00Z
Hello, 世界 👋
### Assistant — 1970-01-01T00:00:01Z
| Item | Value |
| --- | --- |
| code | `ok` |
```swift
print("hi")
```
### User — 1970-01-01T00:00:02Z
_[attachment: résumé.txt]_
""")
}
@Test(arguments: [
"file", "attachment", "image", "audio", "video",
"FILE", "ATTACHMENT", "IMAGE", "AuDiO", "ViDeO",
])
func `classifies gateway media and historical file attachments consistently`(type: String) {
let content = OpenClawChatMessageContent(
type: type,
text: nil,
mimeType: nil,
fileName: nil,
content: nil)
#expect(content.isInlineAttachment)
}
@Test(arguments: ["text", "canvas", "toolCall", "tool_result"])
func `does not classify unrelated content as an attachment`(type: String) {
let content = OpenClawChatMessageContent(
type: type,
text: nil,
mimeType: "image/png",
fileName: "not-an-attachment.png",
content: nil)
#expect(!content.isInlineAttachment)
}
@Test func `does not classify content without a type as an attachment`() {
let content = OpenClawChatMessageContent(
type: nil,
text: nil,
mimeType: "image/png",
fileName: "not-an-attachment.png",
content: nil)
#expect(!content.isInlineAttachment)
}
@Test(arguments: ["user", "assistant"], ["image", "audio", "video", "IMAGE", "AuDiO", "ViDeO"])
func `exports canonical media-only messages`(role: String, type: String) {
let isAudio = type.lowercased() == "audio"
let isVideo = type.lowercased() == "video"
let filename = isAudio ? "voice-note.m4a" : isVideo ? "clip.mp4" : "photo.png"
let message = OpenClawChatMessage(
role: role,
content: [
OpenClawChatMessageContent(
type: type,
text: nil,
mimeType: isAudio ? "audio/mp4" : isVideo ? "video/mp4" : "image/png",
fileName: filename,
content: nil),
],
timestamp: 0)
let markdown = ChatTranscriptExporter.markdown(
sessionTitle: "Media chat",
sessionKey: "agent:main",
messages: [message])
#expect(markdown == """
# Media chat
### \(role == "user" ? "User" : "Assistant") — 1970-01-01T00:00:00Z
_[attachment: \(filename)]_
""")
}
@Test(arguments: ["image", "audio", "IMAGE", "AuDiO"])
func `preserves canonical media alongside message text`(type: String) {
let isAudio = type.lowercased() == "audio"
let filename = isAudio ? "voice-note.m4a" : "photo.png"
let message = OpenClawChatMessage(
role: "user",
content: [
OpenClawChatMessageContent(
type: "text",
text: "Here is the attachment.",
mimeType: nil,
fileName: nil,
content: nil),
OpenClawChatMessageContent(
type: type,
text: nil,
mimeType: isAudio ? "audio/mp4" : "image/png",
fileName: filename,
content: nil),
],
timestamp: 0)
let markdown = ChatTranscriptExporter.markdown(
sessionTitle: "Media chat",
sessionKey: "agent:main",
messages: [message])
#expect(markdown == """
# Media chat
### User — 1970-01-01T00:00:00Z
Here is the attachment.
_[attachment: \(filename)]_
""")
}
@Test(arguments: ["canvas", "toolCall", "tool_result"])
func `does not export non-attachment content as media`(type: String) {
let message = OpenClawChatMessage(
role: "assistant",
content: [
OpenClawChatMessageContent(
type: type,
text: nil,
mimeType: "image/png",
fileName: "not-an-attachment.png",
content: nil),
],
timestamp: 0)
#expect(ChatTranscriptExporter.markdown(
sessionTitle: "Media chat",
sessionKey: "agent:main",
messages: [message]) == "# Media chat\n")
}
@Test func `sanitizes filename`() {
#expect(
ChatTranscriptExporter.filename(
sessionTitle: " Project / Q3: \"Résumé\"? ",
sessionKey: "fallback") == "Project-Q3-Résumé.md")
#expect(
ChatTranscriptExporter.filename(
sessionTitle: "///",
sessionKey: "fallback") == "fallback.md")
let emojiFilename = ChatTranscriptExporter.filename(
sessionTitle: String(repeating: "🐾", count: 200),
sessionKey: "fallback")
#expect(emojiFilename.utf8.count <= 243)
#expect(emojiFilename.hasSuffix(".md"))
}
@Test func `empty transcript contains header only`() {
#expect(
ChatTranscriptExporter.markdown(
sessionTitle: nil,
sessionKey: "agent:main",
messages: []) == "# agent:main\n")
}
private func message(role: String, text: String, timestamp: Double) -> OpenClawChatMessage {
OpenClawChatMessage(
role: role,
content: [
OpenClawChatMessageContent(
type: "text",
text: text,
mimeType: nil,
fileName: nil,
content: nil),
],
timestamp: timestamp)
}
}