package cc.unitmesh.devins.idea.renderer import cc.unitmesh.devins.llm.MessageRole import cc.unitmesh.agent.render.TimelineItem import cc.unitmesh.agent.render.TaskStatus import cc.unitmesh.llm.compression.TokenInfo import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue /** * Unit tests for JewelRenderer. * * Tests the renderer's functionality including: * - Timeline management * - Message rendering * - Tool call handling * - Error handling * - Task completion * * These tests do not require IntelliJ Platform and can be run standalone. */ class JewelRendererTest { private lateinit var renderer: JewelRenderer @BeforeEach fun setUp() { // Disable debouncing for deterministic unit tests renderer = JewelRenderer(streamingUpdateDebounceMs = 0L) } @Test fun testInitialState() = runBlocking { val timeline = renderer.timeline.first() assertTrue(timeline.isEmpty()) val streamingOutput = renderer.currentStreamingOutput.first() assertTrue(streamingOutput.isEmpty()) val isProcessing = renderer.isProcessing.first() assertFalse(isProcessing) } @Test fun testAddUserMessage() = runBlocking { renderer.addUserMessage("Hello, world!") val timeline = renderer.timeline.first() assertEquals(1, timeline.size) val item = timeline.first() assertTrue(item is TimelineItem.MessageItem) assertEquals(MessageRole.USER, (item as TimelineItem.MessageItem).role) assertEquals("Hello, world!", item.content) } @Test fun testLLMResponseStreaming() = runBlocking { renderer.renderLLMResponseStart() val isProcessing = renderer.isProcessing.first() assertTrue(isProcessing) renderer.renderLLMResponseChunk("Hello ") renderer.renderLLMResponseChunk("world!") val streamingOutput = renderer.currentStreamingOutput.first() assertTrue(streamingOutput.contains("Hello")) assertTrue(streamingOutput.contains("world")) renderer.renderLLMResponseEnd() val finalProcessing = renderer.isProcessing.first() assertFalse(finalProcessing) val timeline = renderer.timeline.first() assertTrue(timeline.isNotEmpty()) } @Test fun testToolCallRendering() = runBlocking { renderer.renderToolCall("read-file", "path=\"/test/file.txt\"") val currentToolCall = renderer.currentToolCall.first() assertNotNull(currentToolCall) assertTrue(currentToolCall!!.details?.contains("file.txt") == true) val timeline = renderer.timeline.first() assertEquals(1, timeline.size) assertTrue(timeline.first() is TimelineItem.ToolCallItem) } @Test fun testToolResultRendering() = runBlocking { renderer.renderToolCall("read-file", "path=\"/test/file.txt\"") renderer.renderToolResult( toolName = "read-file", success = true, output = "File content here", fullOutput = "Full file content here with more details", metadata = mapOf("execution_time_ms" to "100") ) val currentToolCall = renderer.currentToolCall.first() assertNull(currentToolCall) val timeline = renderer.timeline.first() assertEquals(1, timeline.size) val toolItem = timeline.first() as TimelineItem.ToolCallItem assertTrue(toolItem.success == true) assertNotNull(toolItem.output) } @Test fun testErrorRendering() = runBlocking { renderer.renderError("Something went wrong!") val errorMessage = renderer.errorMessage.first() assertEquals("Something went wrong!", errorMessage) val timeline = renderer.timeline.first() assertEquals(1, timeline.size) assertTrue(timeline.first() is TimelineItem.ErrorItem) } @Test fun testTaskCompletion() = runBlocking { renderer.renderFinalResult(true, "Task completed successfully", 5) val timeline = renderer.timeline.first() assertEquals(1, timeline.size) val item = timeline.first() as TimelineItem.TaskCompleteItem assertTrue(item.success) assertEquals("Task completed successfully", item.message) assertEquals(5, item.iterations) } @Test fun testClearTimeline() = runBlocking { renderer.addUserMessage("Message 1") renderer.addUserMessage("Message 2") renderer.renderError("An error") var timeline = renderer.timeline.first() assertEquals(3, timeline.size) renderer.clearTimeline() timeline = renderer.timeline.first() assertTrue(timeline.isEmpty()) val errorMessage = renderer.errorMessage.first() assertNull(errorMessage) } @Test fun testIterationTracking() = runBlocking { renderer.renderIterationHeader(3, 10) val currentIteration = renderer.currentIteration.first() assertEquals(3, currentIteration) val maxIterations = renderer.maxIterations.first() assertEquals(10, maxIterations) } @Test fun testTokenInfoUpdate() = runBlocking { val tokenInfo = TokenInfo( totalTokens = 100, inputTokens = 60, outputTokens = 40 ) renderer.updateTokenInfo(tokenInfo) val totalTokenInfo = renderer.totalTokenInfo.first() assertEquals(100, totalTokenInfo.totalTokens) assertEquals(60, totalTokenInfo.inputTokens) assertEquals(40, totalTokenInfo.outputTokens) } @Test fun testForceStop() = runBlocking { renderer.renderLLMResponseStart() renderer.renderLLMResponseChunk("Partial output") assertTrue(renderer.isProcessing.first()) renderer.forceStop() assertFalse(renderer.isProcessing.first()) // Verify the interrupted message was added val timeline = renderer.timeline.first() assertTrue(timeline.isNotEmpty()) val lastItem = timeline.last() assertTrue(lastItem is TimelineItem.MessageItem) assertTrue((lastItem as TimelineItem.MessageItem).content.contains("[Interrupted]")) } @Test fun testTaskBoundaryToolUpdatesTaskList() = runBlocking { renderer.renderToolCall("task-boundary", "taskName=\"Build\" status=\"WORKING\" summary=\"Building project\"") val tasks = renderer.tasks.first() assertEquals(1, tasks.size) assertEquals("Build", tasks.first().taskName) assertEquals(TaskStatus.WORKING, tasks.first().status) } }