1package soy.iko.opencode.ui.chat
3import org.junit.Assert.assertEquals
4import org.junit.Assert.assertTrue
6import soy.iko.opencode.data.model.AssistantMessage
7import soy.iko.opencode.data.model.MessageWithParts
8import soy.iko.opencode.data.model.ReasoningPart
9import soy.iko.opencode.data.model.TextPart
10import soy.iko.opencode.data.model.ToolCompleted
11import soy.iko.opencode.data.model.ToolPart
12import soy.iko.opencode.data.model.UserMessage
14class ConversationExportTest {
16 private val sid = "s1"
18 private fun user(id: String, text: String) =
19 MessageWithParts(UserMessage(id, sid), listOf(TextPart(id = "p$id", text = text)))
21 private fun assistant(id: String, text: String) =
22 MessageWithParts(AssistantMessage(id, sid), listOf(TextPart(id = "p$id", text = text)))
25 fun rendersRoleHeadersAndBodies() {
26 val md = buildConversationMarkdown(
27 listOf(user("u1", "Hello"), assistant("a1", "Hi there!")),
30 assertTrue(md.startsWith("# My chat"))
31 assertTrue(md.contains("## You\n\nHello"))
32 assertTrue(md.contains("## opencode\n\nHi there!"))
36 fun omitsTitleWhenBlank() {
37 val md = buildConversationMarkdown(listOf(user("u1", "hi")), title = null)
38 // The metadata header is now prepended (message count + timestamp range), so the
39 // first message heading follows it.
40 assertTrue(md.contains("## You"))
44 fun skipsMessagesWithoutText() {
45 val onlyReasoning = MessageWithParts(
46 AssistantMessage("a1", sid),
47 listOf(ReasoningPart(id = "r1", text = "hmm")),
49 val md = buildConversationMarkdown(listOf(onlyReasoning), title = null)
50 // Reasoning is exported in italics, so it still appears.
51 assertTrue(md.contains("## opencode"))
52 assertTrue(md.contains("_hmm_"))
56 fun emptyConversationProducesJustTheTitle() {
57 // No message bodies to export, but the title header is still emitted.
58 assertEquals("# x", buildConversationMarkdown(emptyList(), title = "x"))
62 fun includesToolCallSummary() {
63 val assistant = MessageWithParts(
64 AssistantMessage("a1", sid),
66 TextPart(id = "t1", text = "Done"),
67 ToolPart(id = "tool1", tool = "read", state = ToolCompleted(output = "file contents")),
70 val md = buildConversationMarkdown(listOf(user("u1", "read the file"), assistant), title = null)
71 assertTrue(md.contains("## opencode"))
72 assertTrue("tool name should appear", md.contains("**read**"))
73 assertTrue("tool output should appear", md.contains("file contents"))
77 fun includesToolCallWithTitle() {
78 val assistant = MessageWithParts(
79 AssistantMessage("a1", sid),
84 state = ToolCompleted(title = "Editing src/main.kt", output = "ok"),
88 val md = buildConversationMarkdown(listOf(assistant), title = null)
89 assertTrue(md.contains("**edit**"))
90 assertTrue(md.contains("Editing src/main.kt"))
94 fun buildMessageMarkdownRendersAssistantReply() {
95 val md = buildMessageMarkdown(assistant("a1", "Hi there!"))
97 assertTrue(md!!.startsWith("## opencode"))
98 assertTrue(md.contains("Hi there!"))
102 fun buildMessageMarkdownRendersUserMessage() {
103 val md = buildMessageMarkdown(user("u1", "Hello"))
105 assertTrue(md!!.startsWith("## You"))
106 assertTrue(md.contains("Hello"))
110 fun buildMessageMarkdownReturnsNullForEmptyMessage() {
111 val empty = MessageWithParts(
112 AssistantMessage("a1", sid),
113 listOf(ReasoningPart(id = "r1", text = "")),
115 assertNull(buildMessageMarkdown(empty))
118 private fun assertNotNull(value: String?) = org.junit.Assert.assertNotNull(value)
119 private fun assertNull(value: String?) = org.junit.Assert.assertNull(value)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list