Изменение выносит типы контента в :content-api и разделяет потоки событий.

This commit is contained in:
2026-09-30 12:41:10 +03:00
parent 26f94ea83d
commit 6b7b5ee57b
11 changed files with 95 additions and 63 deletions
@@ -28,14 +28,14 @@ class LiveTurnTest {
fun `full happy path accumulates reasoning and answer`() {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
OnlineEvent.StartReasoning(at()),
OnlineEvent.AppendText(at(), "сначала "),
OnlineEvent.AppendText(at(), "подумаем"),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "Привет, "),
OnlineEvent.AppendText(at(), "мир!"),
Event.End(at()),
OnlineEvent.End(at()),
)
assertEquals(TurnPhase.DONE, turn.phase)
@@ -52,11 +52,11 @@ class LiveTurnTest {
fun `tool call and result are stitched into one block`() {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
Event.ToolCall(at(), id = "t1", title = "grep", toolName = "grep", toolArgs = "{\"q\":\"x\"}"),
Event.ToolResult(at(), toolCallId = "t1", toolName = "grep", result = "found 3"),
Event.End(at()),
OnlineEvent.End(at()),
)
val tools = turn.blocks.filterIsInstance<LiveBlock.Tool>()
@@ -72,7 +72,7 @@ class LiveTurnTest {
fun `orphan tool result creates a block`() {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
Event.ToolResult(at(), toolCallId = "missing", toolName = null, result = "ok"),
)
@@ -87,7 +87,7 @@ class LiveTurnTest {
fun `tool failed marks the block`() {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
Event.ToolCall(at(), id = "t9", title = null, toolName = "bash", toolArgs = "{}"),
Event.ToolFailed(at(), toolCallId = "t9", toolName = "bash", message = "boom", durationMs = 5),
)
@@ -100,7 +100,7 @@ class LiveTurnTest {
fun `error appends failure block and fails the turn`() {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "partial"),
Event.Error(at(), message = "llm down", code = "E500"),
@@ -118,7 +118,7 @@ class LiveTurnTest {
fun `interrupted turn is terminal and keeps partial text`() {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "не до конца"),
Event.Interrupted(at()),
@@ -133,7 +133,7 @@ class LiveTurnTest {
fun `text after tool call starts a new answer block`() {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "до"),
Event.ToolCall(at(), id = "t1", title = null, toolName = "grep", toolArgs = "{}"),
@@ -152,10 +152,10 @@ class LiveTurnTest {
val bytes = byteArrayOf(1, 2, 3)
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.IMAGE),
OnlineEvent.AppendImage(at(), body = bytes, mime = "image/png"),
Event.End(at()),
OnlineEvent.End(at()),
)
assertTrue(turn.blocks.filterIsInstance<LiveBlock.Answer>().isEmpty())
@@ -168,12 +168,12 @@ class LiveTurnTest {
fun `working resets the previous turn`() {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
OnlineEvent.StartResponse(at(), OnlineEvent.ResponseType.TEXT),
OnlineEvent.AppendText(at(), "старое"),
Event.End(at()),
OnlineEvent.End(at()),
)
turn.feed(Event.Working(at()), OnlineEvent.AppendText(at(), "новое"))
turn.feed(OnlineEvent.Working(at()), OnlineEvent.AppendText(at(), "новое"))
assertEquals(1, turn.blocks.size)
assertEquals("новое", turn.answerText())
@@ -182,7 +182,7 @@ class LiveTurnTest {
@Test
fun `append without explicit start response still streams as answer`() {
val turn = LiveTurn()
turn.feed(Event.Working(at()), OnlineEvent.AppendText(at(), "без старта"))
turn.feed(OnlineEvent.Working(at()), OnlineEvent.AppendText(at(), "без старта"))
assertEquals(TurnPhase.RESPONDING, turn.phase)
assertEquals("без старта", turn.answerText())
@@ -193,7 +193,7 @@ class LiveTurnTest {
val turn = LiveTurn()
var count = 0
turn.onChange = { count++ }
turn.feed(Event.Working(at()), OnlineEvent.AppendText(at(), "x"))
turn.feed(OnlineEvent.Working(at()), OnlineEvent.AppendText(at(), "x"))
assertEquals(2, count)
}
@@ -202,13 +202,13 @@ class LiveTurnTest {
fun `preview text collapses whitespace and truncates`() {
val turn = LiveTurn()
turn.feed(
Event.Working(at()),
OnlineEvent.Working(at()),
OnlineEvent.AppendText(at(), "Первая строка\n\nвторая строка "),
)
assertEquals("Первая строка вторая строка", turn.previewText())
val long = LiveTurn()
long.feed(Event.Working(at()), OnlineEvent.AppendText(at(), "слово ".repeat(60)))
long.feed(OnlineEvent.Working(at()), OnlineEvent.AppendText(at(), "слово ".repeat(60)))
val preview = long.previewText(maxLength = 30)
assertTrue(preview.length <= 31)
assertTrue(preview.endsWith("…"))