superintelligence-android / android/app/src/test/java/net/kentonc/si/ToolCallAccumulatorTest.kt

raw file 3323 bytes
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)
    }
}