package net.kentonc.si import kotlinx.serialization.json.Json import net.kentonc.si.agent.DeltaFunction import net.kentonc.si.agent.FunctionCall import net.kentonc.si.agent.StreamChunk import net.kentonc.si.agent.ToolCall import net.kentonc.si.agent.ToolCallDelta import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test /** Verifies the SSE side of the agent: fragmented tool-call reassembly and * stream chunk decoding — the fiddliest wire-format code in LlmClient.kt. */ class ToolCallAccumulatorTest { @Test fun fragmentsReassembleByIndex() { val acc = net.kentonc.si.agent.ToolCallAccumulator() acc.push(listOf( ToolCallDelta(index = 0, id = "call_a", function = DeltaFunction(name = "read_file", arguments = "{\"pat")), )) acc.push(listOf( ToolCallDelta(index = 0, function = DeltaFunction(arguments = "h\":\"notes.txt\"}")), ToolCallDelta(index = 1, id = "call_b", function = DeltaFunction(name = "list_dir")), )) val calls = acc.finish() assertEquals(2, calls.size) assertEquals("call_a", calls[0].id) assertEquals("read_file", calls[0].function.name) assertEquals("{\"path\":\"notes.txt\"}", calls[0].function.arguments) assertEquals("call_b", calls[1].id) assertEquals("list_dir", calls[1].function.name) } @Test fun missingIdsGetSynthesized() { val acc = net.kentonc.si.agent.ToolCallAccumulator() acc.push(listOf(ToolCallDelta(index = 0, function = DeltaFunction(name = "run_shell")))) val calls = acc.finish() assertEquals(listOf("call_0"), calls.map { it.id }) } @Test fun outOfOrderIndicesSortOnFinish() { val acc = net.kentonc.si.agent.ToolCallAccumulator() acc.push(listOf(ToolCallDelta(index = 2))) acc.push(listOf(ToolCallDelta(index = 0))) val calls = acc.finish() assertEquals(2, calls.size) // Sorted by wire index; synthesized ids are positional. assertEquals(listOf("call_0", "call_1"), calls.map { it.id }) } } /** Minimal SSE chunk decode + WireMessage serialization round-trip. */ class WireSerializationTest { private val json = Json { ignoreUnknownKeys = true } @Test fun chunkDecodes() { val frame = """{"choices":[{"delta":{"content":"hel"}}]}""" val chunk = json.decodeFromString(StreamChunk.serializer(), frame) assertEquals(1, chunk.choices.size) net.kentonc.si.agent.flattenContent(chunk.choices[0].delta!!.content).let { assertNotNull(it); assertEquals("hel", it) } } @Test fun toolCallMessageRoundTrips() { val message = net.kentonc.si.agent.WireMessage.assistant( content = null, toolCalls = listOf(ToolCall(id = "c1", function = FunctionCall(name = "f", arguments = "{}"))), ) val encoded = json.encodeToString(net.kentonc.si.agent.WireMessage.serializer(), message) val decoded = json.decodeFromString(net.kentonc.si.agent.WireMessage.serializer(), encoded) assertEquals(1, decoded.toolCalls.size) assertEquals("c1", decoded.toolCalls[0].id) assertTrue("tool_calls should be serialized", "\"tool_calls\"" in encoded) } }