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)
}
}